MessageBird Inbound SMS with Next.js: Two-Way Messaging Guide
Build a Next.js application to receive inbound SMS from MessageBird using webhooks, Flow Builder, and API routes. Includes security, database integration, and Vercel deployment.
Build a Next.js application that receives inbound SMS messages sent to your MessageBird virtual number. Create a webhook endpoint using Next.js API routes, configure MessageBird's Flow Builder to forward incoming messages, and deploy to Vercel.
Use this to build automated responders, two-way chat systems, notification triggers, or data collection services via text message.
What You'll Build
Next.js project with secure webhook endpoint
MessageBird Flow Builder configuration for SMS routing
Message processing and logging system
Webhook security using shared secrets
Production deployment on Vercel
Local testing environment with ngrok
Technologies Used
Next.js: A React framework for building server-rendered or statically exported applications, including API routes. Chosen for its ease of development and deployment, especially with Vercel.
MessageBird: A communication platform providing APIs for SMS, voice, and chat. Used here for its virtual numbers and Flow Builder to handle inbound SMS routing.
Node.js: The runtime environment for Next.js.
Vercel: A platform for deploying frontend frameworks and static sites, offering seamless integration with Next.js and easy management of serverless functions (API routes).
ngrok: A tool to expose local development servers to the internet, essential for testing webhooks locally.
System Architecture
sequenceDiagram
participant User as User's Phone
participant MBird as MessageBird Platform
participant FlowBuilder as MessageBird Flow Builder
participant Ngrok as ngrok Tunnel (Dev)
participant VercelApp as Next.js App on Vercel/Local
participant API as API Route (/api/messagebird-webhook)
participant DB as Database (Optional)
User->>+MBird: Sends SMS to Virtual Number
MBird->>+FlowBuilder: Inbound SMS received
FlowBuilder->>+VercelApp: HTTP POST to Webhook URL (via Ngrok in Dev)
Note over VercelApp, API: Request hits Next.js App
VercelApp->>+API: Route request to /api/messagebird-webhook
API->>+API: Validate Request (e.g., check secret)
API->>+DB: Process & Store Message (Optional)
API-->>-VercelApp: Return HTTP 200 OK
VercelApp-->>-FlowBuilder: Forward HTTP 200 OK
FlowBuilder-->>-MBird: Flow execution complete
MBird-->>-User: (No direct response unless configured)
(Ensure your publishing platform supports Mermaid diagram rendering)
Prerequisites
A MessageBird account with a purchased virtual mobile number capable of receiving SMS.
Node.js (v18 or later recommended) – required for Next.js 14+ and optimal compatibility.
npm or yarn package manager.
A Vercel account (free tier is sufficient).
ngrok installed globally or available via npx for local testing.
Basic familiarity with JavaScript, React, Next.js, and terminal commands.
Note on Next.js Versions:
This guide is compatible with Next.js 14 and 15. If using Next.js 15, be aware that request-specific APIs (headers, cookies, params, searchParams) are now asynchronous, which may require adjustments if extending this implementation beyond the webhook handler shown.
Final Outcome
A deployed Next.js application with a publicly accessible API endpoint that logs incoming SMS messages sent to your configured MessageBird number. The endpoint will be secured with a basic shared secret.
1. Setting up the Project
Create a new Next.js application with the required configuration.
1.1 Create Next.js App:
Run this command in your terminal, replacing messagebird-inbound-app with your project name:
Create .env.local in your project root to store sensitive configuration. Never commit this file to version control. It should be in .gitignore by default.
# .env.local
# A secret shared between your app and MessageBird Flow Builder to verify requests
MESSAGEBIRD_WEBHOOK_SECRET=your_generated_secret_string_here
Configuration Purpose:
create-next-app: Scaffolds a modern Next.js application with best practices.
.env.local: Stores environment-specific variables securely, keeping secrets out of your codebase. MESSAGEBIRD_WEBHOOK_SECRET verifies that webhook requests originate from MessageBird.
2. Implementing the Webhook Handler
Create an API route in your Next.js application to receive webhook calls from MessageBird.
2.1 Create the API Route File:
Inside the src/app/api/ directory, create a new folder named messagebird-webhook. Inside this folder, create a file named route.js.
Paste the following code into src/app/api/messagebird-webhook/route.js:
JavaScript
// src/app/api/messagebird-webhook/route.jsimport { NextResponse } from'next/server';
/**
* Handles POST requests from MessageBird's Flow Builder for incoming SMS.
* Expects form-encoded data.
* @param {Request} request The incoming request object.
* @returns {NextResponse} A response object.
*/exportasyncfunctionPOST(request) {
console.log('Received request on /api/messagebird-webhook');
const secret = process.env.MESSAGEBIRD_WEBHOOK_SECRET;
if (!secret) {
console.error('MESSAGEBIRD_WEBHOOK_SECRET is not set in environment variables.');
// Return 200 OK to avoid MessageBird retries on config errors,// but log the server-side issue.returnnewNextResponse('Configuration error', { status: 200 });
}
// --- Security Check ---// Get the secret from the query parameter (adjust if using headers)const url = newURL(request.url);
const providedSecret = url.searchParams.get('secret');
if (providedSecret !== secret) {
console.warn('Invalid or missing secret in webhook request.');
// Respond with 403 Forbidden if the secret is wrongreturnnewNextResponse('Forbidden', { status: 403 });
}
console.log('Webhook secret validated successfully.');
try {
// MessageBird sends data as application/x-www-form-urlencodedconst formData = await request.formData();
const messageData = Object.fromEntries(formData);
// --- Log Incoming Message Data ---// Key fields based on MessageBird documentation for SMS webhooks:// - originator: The sender's phone number (e.g., +14155552671)// - recipient: Your MessageBird virtual number// - payload: The content of the SMS message// - messageId: Unique ID for the message// - createdDatetime: Timestamp of message creation// (Note: Field names can sometimes vary slightly based on MessageBird// configuration or message type. Logging the full object is recommended// to confirm the exact fields received in your specific setup. Common// fields include 'originator', 'payload', 'recipient', 'messageId', 'createdDatetime'.)console.log('Received MessageBird Payload:', JSON.stringify(messageData, null, 2));
const originator = messageData.originator;
const payload = messageData.payload;
const recipient = messageData.recipient; // Your virtual numberconst messageId = messageData.messageId;
if (!originator || !payload) {
console.warn('Missing required fields (originator or payload) in webhook data.');
// Return 400 Bad Request if essential data is missingreturnnewNextResponse('Bad Request: Missing required fields', { status: 400 });
}
// --- Process the Message (Example: Logging) ---// In a real application, you would:// 1. Store the message in a database.// 2. Trigger business logic (e.g., auto-reply, notification).// 3. Queue for further processing if needed.console.log(`Processing message ${messageId} from ${originator} to ${recipient}: "${payload}"`);
// --- Respond to MessageBird ---// Important: Respond quickly with a 200 OK to acknowledge receipt.// MessageBird doesn't process the response body for SMS webhooks.// Avoid long-running tasks here; defer them if necessary.returnnewNextResponse('Webhook received successfully', { status: 200 });
} catch (error) {
console.error('Error processing MessageBird webhook:', error);
// Return 500 Internal Server Error for unexpected issues// Consider returning 200 OK even on errors if you don't want MessageBird// to retry, but ensure you have robust logging/alerting.returnnewNextResponse('Internal Server Error', { status: 500 });
}
}
/**
* Handles GET requests (optional, e.g., for simple health checks).
* @param {Request} request The incoming request object.
* @returns {NextResponse} A response object.
*/exportasyncfunctionGET(request) {
console.log('Received GET request on /api/messagebird-webhook');
// You could add a health check herereturnnewNextResponse('API endpoint is active. Use POST for webhooks.', { status: 200 });
}
Code Explanation:
Import NextResponse: Used for sending responses from API routes.
POST Function: This is the primary handler for incoming webhook requests from MessageBird, which uses the POST HTTP method for SMS webhooks.
Environment Variable Check: Checks if the MESSAGEBIRD_WEBHOOK_SECRET is loaded correctly. If not, it logs an error but returns a 200 OK to prevent MessageBird from endlessly retrying due to a server configuration issue. You must fix the environment variable setup if this occurs.
Security Check:
Retrieves the secret query parameter from the incoming request URL (e.g., https://yourapp.com/api/messagebird-webhook?secret=your_secret).
Compares this providedSecret with the one stored in your environment variables (process.env.MESSAGEBIRD_WEBHOOK_SECRET).
If they don't match, it logs a warning and returns a 403 Forbidden status, rejecting the request.
Parsing Form Data: MessageBird sends SMS webhook data as application/x-www-form-urlencoded. request.formData() parses this into a FormData object, which we convert to a plain JavaScript object (messageData).
Logging: The entire received payload is logged as a JSON string for debugging. This helps confirm the exact field names MessageBird is sending (originator, payload, recipient, etc.).
Data Validation: Checks for the presence of essential fields (originator, payload). If missing, it returns a 400 Bad Request.
Processing Logic (Placeholder): This is where you'd add your application-specific logic, like saving the message to a database or triggering other actions. For now, it just logs the key details.
Success Response: Crucially, it returns a 200 OK response using NextResponse. MessageBird expects this to confirm successful receipt. The response body is ignored by MessageBird for SMS webhooks.
Error Handling: A try...catch block catches any unexpected errors during processing, logs them, and returns a 500 Internal Server Error.
GET Function (Optional): A simple handler for GET requests is included. This isn't used by MessageBird for SMS webhooks but can be useful for manually checking if the endpoint is reachable or for setting up simple external health checks.
3. API Endpoint Details (Webhook)
While not a traditional user-facing API, the webhook endpoint acts as an API for MessageBird.
Endpoint:/api/messagebird-webhook
Method:POST
Authentication: Shared secret passed as a URL query parameter (?secret=YOUR_SECRET).
Request Body Format:application/x-www-form-urlencoded
Expected Request Body Parameters (Key Fields):
originator: String (Sender's phone number in international format)
(Log the full payload to see all available fields)
Success Response:200 OK (Empty body or simple text confirmation)
Error Responses:
403 Forbidden: Invalid or missing secret query parameter.
400 Bad Request: Missing required fields (originator or payload).
500 Internal Server Error: Unexpected server-side error during processing.
200 OK (with server-side error logged): If MESSAGEBIRD_WEBHOOK_SECRET is not configured.
Testing with curl (Simulating MessageBird):
You'll need your local server running (npm run dev) and ngrok exposing it (see Section 4). Replace YOUR_NGROK_URL and YOUR_SECRET accordingly. Use a realistic past or generic date for createdDatetime.
The command above uses double quotes for data fields and the URL. This generally works in bash/zsh. If you encounter issues in other shells or if your secret contains special shell characters, try putting single quotes around the entire URL: 'YOUR_NGROK_URL/api/messagebird-webhook?secret=YOUR_SECRET'.
Check your terminal running npm run dev for the log output from the route.js handler.
4. Integrating with MessageBird
Configure MessageBird to send incoming SMS messages to your Next.js application.
4.1 Local Development Setup (ngrok):
MessageBird needs a publicly accessible URL to send webhooks to. During development, your local machine isn't typically accessible. Use ngrok to create a secure tunnel.
Start your Next.js dev server:
Bash
npm run dev
It usually runs on http://localhost:3000.
Start ngrok:
Open another terminal window and run:
Bash
ngrok http 3000
Copy the ngrok URL:
ngrok will display forwarding URLs. Copy the https URL (e.g., https://random-subdomain.ngrok-free.app). This is your temporary public URL.
4.2 Configure MessageBird Flow Builder:
Log in to your MessageBird Dashboard.
Navigate to Flow Builder from the left-hand menu.
Click Create new flow > Create Custom Flow.
Give your flow a name (e.g., "Next.js Inbound SMS").
Choose SMS as the trigger. Click Next.
Configure the Trigger:
Click the "SMS" trigger step.
Select the MessageBird virtual Number(s) you want to use for receiving messages.
Click Save.
Add the Webhook Step:
Click the + icon below the SMS trigger.
Search for and select the Call HTTP endpoint with SMS step.
Configure the HTTP Endpoint Step:
URL: Paste your ngrok https URL (from step 4.1.3) and append the API route path and the secret query parameter:
https://your-random-subdomain.ngrok-free.app/api/messagebird-webhook?secret=your_generated_secret_string_here(Replace the URL and secret with your actual values from .env.local)
Method: Select POST.
Keep Parameters: Ensure this is checked (it usually is by default). This forwards the SMS data.
Click Save.
Publish the Flow:
Click the Publish button in the top-right corner. Confirm the publication.
Diagram of Flow Builder Setup:
graph LR
A[SMS Trigger: Your Number] --> B(Call HTTP endpoint with SMS);
B -- POST Request --> C{Your Webhook URL\n(ngrok/Vercel)};
(Ensure your publishing platform supports Mermaid diagram rendering)
Explanation of Environment Variables:
MESSAGEBIRD_WEBHOOK_SECRET (used in .env.local and later in Vercel): This secret string is added as a query parameter to the webhook URL configured in Flow Builder. The Next.js API route verifies this secret upon receiving a request, ensuring that only requests knowing the secret (presumably only MessageBird via your Flow Builder config) are processed.
Testing Local Integration:
Send an SMS message from your phone to the MessageBird virtual number you configured in Flow Builder.
Watch the terminal running npm run dev. You should see the console.log output from your route.js file, including the "Webhook secret validated successfully" message and the "Received MessageBird Payload".
Watch the terminal running ngrok. You should see an incoming POST request to /api/messagebird-webhook with a 200 OK response status.
If it works, your local setup is correctly receiving messages!
5. Implementing Error Handling and Logging
The route.js file already includes basic error handling and logging:
Configuration Errors: Checks for missing MESSAGEBIRD_WEBHOOK_SECRET. Logs error server-side, returns 200 OK to MessageBird.
Data Validation Errors: Checks for missing required fields. Logs warning, returns 400 Bad Request.
Runtime Errors:try...catch block captures unexpected errors during processing. Logs error, returns 500 Internal Server Error.
Logging: Uses console.log, console.warn, and console.error for different levels of information. The entire payload is logged for debugging.
Improvements for Production:
Structured Logging:
Use a dedicated logging library like pino for structured JSON logs, which are easier to parse and analyze in log management systems.
Bash
npm install pino pino-pretty # pino-pretty for dev
Adapt route.js to use pino.
Error Tracking Services:
Integrate with services like Sentry or Bugsnag to capture, report, and analyze errors automatically.
Retry Mechanisms (Application Level):
If your processing logic involves external services that might fail temporarily, implement retry logic (e.g., using async-retry) within your handler after sending the 200 OK to MessageBird, or preferably by pushing the message to a queue (like Redis, RabbitMQ, or AWS SQS) for background processing with retries. Do not delay the 200 OK response to MessageBird.
Testing Error Scenarios:
Invalid Secret:
Send a curl request (Section 3) with the wrong or no secret parameter. Expect a 403 Forbidden.
Missing Payload:
Send a curl request without the originator or payload form data fields. Expect a 400 Bad Request.
Simulate Internal Error:
Temporarily add throw new Error('Simulated processing error'); inside the try block before the final return. Send a valid request. Expect a 500 Internal Server Error response and an error log in the console.
6. Creating a Database Schema and Data Layer (Optional)
For most real-world applications, you'll want to store incoming messages. Let's add basic persistence using Prisma and SQLite.
⚠️ Critical Production Warning – SQLite on Vercel:
SQLite is NOT supported for production deployments on Vercel or other serverless platforms. This is a fundamental architectural incompatibility, not a configuration issue:
Ephemeral Filesystem: Vercel serverless functions have ephemeral (temporary) storage that is wiped between deployments and during auto-scaling. The SQLite database file will be lost.
No Shared Storage: Each serverless function instance runs in isolation and cannot share a single SQLite file across multiple concurrent invocations.
Data Loss Risk: Any data written to SQLite during a function execution may disappear when that instance is recycled.
For Production on Vercel (as of 2025):
Prisma Postgres: Recommended serverless database with no cold starts, built-in connection pooling, and native Vercel integration. Can be provisioned directly from Vercel Dashboard.
Other Hosted Databases: Vercel Postgres, Neon, PlanetScale, Supabase, AWS RDS, or any PostgreSQL/MySQL database with connection pooling support.
Connection Pooling: Essential for serverless environments where each function invocation may create a new database connection.
The following steps demonstrate SQLite for local development and learning purposes only. Replace with a proper database service before deploying to production.
This creates a prisma directory with a schema.prisma file and updates .env.local with a DATABASE_URL.
6.3 Define Schema:
Open prisma/schema.prisma and define a model for incoming messages:
Prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
// Points to the file path in DATABASE_URL from .env.local
// e.g., DATABASE_URL="file:./dev.db"
url = env("DATABASE_URL")
}
model IncomingMessage {
id String @id @default(cuid()) // Unique ID for the DB record
messageBirdId String @unique // Store MessageBird's unique ID
originator String // Sender's phone number
recipient String // Your virtual number
payload String // Message content
receivedAt DateTime @default(now()) // Timestamp when we stored it
messageBirdTs DateTime? // Optional: Store MessageBird's timestamp if available
}
6.4 Create Initial Migration:
Bash
npx prisma migrate dev --name init
This creates the SQLite database file (e.g., prisma/dev.db based on your DATABASE_URL) and the IncomingMessage table. Make sure prisma/dev.db is added to your .gitignore.
6.5 Implement Data Layer:
Create a utility function to instantiate and share the Prisma client instance.
JavaScript
// src/lib/prisma.js (Create this file)import { PrismaClient } from'@prisma/client';
let prisma;
if (process.env.NODE_ENV === 'production') {
// In production, always create a new instance// Note: Consider implications for serverless function reuse vs. connection limits
prisma = newPrismaClient();
} else {
// In development, reuse the instance across hot-reloadsif (!global.prisma) {
global.prisma = newPrismaClient({
// Optionally add logging for development// log: ['query', 'info', 'warn', 'error'],
});
}
prisma = global.prisma;
}
exportdefault prisma;
6.6 Update Webhook Handler to Save Message:
Modify src/app/api/messagebird-webhook/route.js:
JavaScript
// src/app/api/messagebird-webhook/route.jsimport { NextResponse } from'next/server';
import prisma from'@/lib/prisma'; // Import prisma instance// Keep the existing POST function structure up to the security checkexportasyncfunctionPOST(request) {
console.log('Received request on /api/messagebird-webhook');
const secret = process.env.MESSAGEBIRD_WEBHOOK_SECRET;
if (!secret) {
console.error('MESSAGEBIRD_WEBHOOK_SECRET is not set.');
returnnewNextResponse('Configuration error', { status: 200 });
}
const url = newURL(request.url);
const providedSecret = url.searchParams.get('secret');
if (providedSecret !== secret) {
console.warn('Invalid or missing secret.');
returnnewNextResponse('Forbidden', { status: 403 });
}
console.log('Webhook secret validated successfully.');
try {
const formData = await request.formData();
const messageData = Object.fromEntries(formData);
console.log('Received MessageBird Payload:', JSON.stringify(messageData, null, 2));
const originator = messageData.originator;
const payload = messageData.payload;
const recipient = messageData.recipient;
const messageId = messageData.messageId; // MessageBird's IDconst createdDatetime = messageData.createdDatetime; // MessageBird's timestampif (!originator || !payload || !messageId) { // Add messageId checkconsole.warn('Missing required fields (originator, payload, or messageId) in webhook data.');
returnnewNextResponse('Bad Request: Missing required fields', { status: 400 });
}
// --- Save the Message to Database ---try {
const newMessage = await prisma.incomingMessage.create({
data: {
messageBirdId: messageId,
originator: originator,
recipient: recipient || 'unknown', // Handle cases where recipient might be missingpayload: payload,
messageBirdTs: createdDatetime ? newDate(createdDatetime) : null,
// receivedAt is handled by @default(now()) in schema
},
});
console.log(`Message ${newMessage.id} (MB ID: ${messageId}) saved to DB.`);
} catch (dbError) {
// Handle potential unique constraint errors if MessageBird retries// Prisma error code for unique constraint violation is P2002if (dbError.code === 'P2002' && dbError.meta?.target?.includes('messageBirdId')) {
console.warn(`Duplicate message received (MessageBird ID: ${messageId}). Ignoring.`);
// Still return 200 OK as we've effectively processed/acknowledged it beforereturnnewNextResponse('Webhook received (duplicate ignored)', { status: 200 });
} else {
console.error('Database error saving message:', dbError);
// Decide if a DB error should cause a 500 or if you still return 200 OK// Returning 500 might cause MessageBird retries, potentially leading to more duplicates if the issue persists.// Returning 200 prevents retries but might lose data if the DB issue is temporary.// Robust solution: Queue message first, then process/save from queue.// For now, we return 500 to indicate a server-side problem beyond duplicates.returnnewNextResponse('Internal Server Error during DB operation', { status: 500 });
}
}
// --- Respond to MessageBird ---// Respond *after* attempting to save, unless it was a handled duplicate.returnnewNextResponse('Webhook received successfully', { status: 200 });
} catch (error) {
// Catch errors outside the DB block (e.g., parsing formData)console.error('Error processing MessageBird webhook:', error);
returnnewNextResponse('Internal Server Error', { status: 500 });
}
}
// Keep GET functionexportasyncfunctionGET(request) {
console.log('Received GET request on /api/messagebird-webhook');
returnnewNextResponse('API endpoint is active. Use POST for webhooks.', { status: 200 });
}
Key Changes:
Imported the Prisma client instance (@/lib/prisma).
Added a try...catch block specifically for the database operation (prisma.incomingMessage.create).
Used prisma.incomingMessage.create to save the relevant data extracted from messageData.
Included handling for potential duplicate messages by checking for Prisma's unique constraint violation error (P2002) specifically on the messageBirdId field. If it's a duplicate, log a warning and return 200 OK.
Added messageId to the required fields check.
The final 200 OK response is now sent after the database operation attempt (unless it was a handled duplicate).
Retest by sending an SMS. You should see the "saved to DB" log message in your development console. You can inspect the prisma/dev.db file (using tools like DB Browser for SQLite) to see the saved record.
7. Adding Security Features
Beyond the shared secret, consider these:
Input Sanitization:
While Prisma helps prevent SQL injection, if you use the payload elsewhere (e.g., displaying in a UI), sanitize it to prevent Cross-Site Scripting (XSS) attacks (e.g., using libraries like dompurify).
Rate Limiting:
Implement rate limiting on the API route, especially if processing is resource-intensive, to prevent abuse or accidental loops. Libraries like rate-limiter-flexible or platform features (like Vercel's built-in IP rate limiting) can be used.
Timestamp Verification (Optional):
Check the createdDatetime from MessageBird against the current server time. Reject requests that are too old (e.g., > 5 minutes) to mitigate simple replay attacks, but be cautious about potential clock skew between MessageBird's servers and yours.
HTTPS Enforcement:
Vercel deployments enforce HTTPS by default. Always ensure your ngrok tunnel uses https and your production webhook URL uses https. Never use plain http for webhooks handling sensitive data or secrets.
Signature Verification (Advanced):
MessageBird can sign requests for certain webhook types (check their documentation for specifics, as it might not apply to basic Flow Builder SMS webhooks). If available and configurable for your setup, verifying a cryptographic signature (e.g., HMAC-SHA256) using a separate secret provides stronger assurance of authenticity than just a secret in the URL.
Testing Security:
Use curl or a similar tool to send requests without the secret, with an incorrect secret, or with malformed data to confirm your validation logic returns 403 Forbidden or 400 Bad Request appropriately.
If implementing rate limiting, script multiple rapid requests to verify it blocks excess traffic according to your configuration.
8. Handling Special Cases
Character Encoding:
SMS messages use specific encodings (GSM-7 or UCS-2). MessageBird typically handles the decoding and provides the message payload as a standard UTF-8 string in the webhook. Be aware of this if your application needs to interact with external systems expecting specific SMS encodings.
Concatenated Messages (Long SMS):
MessageBird usually reassembles long SMS messages (split into multiple parts over the air) before triggering the webhook. The payload you receive should contain the full message content. Test with messages longer than 160 characters (GSM-7) or 70 characters (UCS-2) to confirm this behavior.
Message Ordering:
Webhooks are delivered over HTTP and network conditions or MessageBird retries can mean they don't necessarily arrive in the exact order the original SMS messages were sent or received by MessageBird. If strict ordering is critical for your application logic, use timestamps (createdDatetime from MessageBird or your receivedAt timestamp) to order messages during processing or retrieval.
Non-Text Content (MMS):
This guide focuses purely on standard SMS text messages. If you need to receive and process MMS messages (containing images, videos, etc.), the webhook payload structure and potentially the Flow Builder configuration will differ significantly. Consult MessageBird's specific MMS documentation.
Internationalization:
Phone numbers (originator, recipient) are generally provided in the standard E.164 format (e.g., +14155552671). Ensure your system stores and processes these numbers correctly. If your application logic depends on the content (payload) of the message (e.g., language detection, keyword analysis), consider internationalization and localization requirements.
9. Implementing Performance Optimizations
For a simple webhook receiver primarily logging or storing data, performance is usually manageable unless facing very high message volumes. Key considerations include:
Vercel Serverless Function Limits (as of 2025):
Understanding platform constraints helps you design within boundaries and avoid runtime failures:
Request/Response Body Size: 4.5 MB limit for both request and response payloads. For larger payloads, use Vercel's streaming functions which don't have this limit.
Function Execution Duration:
Free tier: Up to 10 seconds
Hobby/Pro plans: Up to 60 seconds (configurable)
Enterprise with Fluid Compute: Up to 800 seconds (13.3 minutes)
Function Size: Maximum 250 MB uncompressed (approximately 50 MB compressed) including code and dependencies. This limit is not configurable.
Logging Limits (Updated January 2025):
256 KB per log line
256 individual log lines per request
1 MB total log size per request
Deployment Frequency: 100 deployments per 24-hour period
WebSocket Limitation: Vercel Functions cannot act as WebSocket servers
The absolute most critical performance factor is responding with 200 OK to MessageBird as quickly as possible (ideally within 1-2 seconds). Defer any potentially slow or unreliable operations (like complex database queries, external API calls, sending replies) until after the response has been sent.
Asynchronous Processing / Queues:
For tasks that take longer than a few hundred milliseconds or involve external dependencies, push the incoming message data (or just an ID) onto a message queue (e.g., Redis, RabbitMQ, AWS SQS, Vercel KV Queue) immediately after validation. Have separate background workers process messages from the queue. This decouples message reception from processing, ensuring fast webhook responses.
Database Connection Management:
In serverless environments, manage database connections efficiently. Use connection pooling (Prisma handles this) and be mindful of connection limits, especially on free tiers of managed databases. The Prisma client instantiation pattern shown earlier helps reuse connections in development but creates new ones in production per function invocation, which is generally correct for serverless but needs monitoring under load.
Caching:
If processing involves frequently accessed data that doesn't change often, implement caching (e.g., using Redis, Vercel KV, or in-memory caches with appropriate invalidation) to speed up lookups.
Code Optimization:
Profile your code (using console.time / console.timeEnd or dedicated profiling tools) to identify bottlenecks within the handler if performance issues arise. Optimize algorithms and data structures.
Platform Scaling:
Leverage the auto-scaling capabilities of your deployment platform (Vercel). Ensure your database and any external services can also handle the potential load.
Testing Performance:
Use tools like k6 or artillery.io to simulate high volumes of concurrent webhook requests to your deployed endpoint (not just ngrok).
Monitor response times and error rates in your Vercel dashboard or logging/monitoring tools.
Observe database connection usage and query performance under load.
Frequently asked questions
Use Next.js API routes to create a webhook endpoint. This endpoint will receive incoming SMS messages forwarded from your MessageBird virtual number via the MessageBird Flow Builder. This allows your Next.js application to process incoming texts and react accordingly.
MessageBird Flow Builder is used to route incoming SMS messages to your Next.js webhook. You configure the Flow Builder to trigger the webhook URL with the message data whenever an SMS is received on your designated MessageBird virtual number.
Next.js API routes provide a serverless function environment ideal for handling webhooks. They offer an easy way to create server-side logic without managing a separate backend server, simplifying development and deployment, especially on Vercel.
ngrok is essential during local development. It creates a public tunnel to your local server so MessageBird can reach your webhook endpoint while you are testing. For production, deploy to Vercel and use the Vercel app URL.
First, create a Next.js API route. Then, in your MessageBird Flow Builder, add an HTTP endpoint step. Paste your ngrok HTTPS URL (for local development) or your Vercel app URL (for production) along with the API endpoint path and your secret as a query parameter.
Use ngrok to create a public URL for your local development server. Configure MessageBird Flow Builder with this URL, then send an SMS to your MessageBird number. Observe logs in your Next.js terminal for successful webhook delivery. Also, you can simulate MessageBird POST requests with curl. Check the logs for validation success and received payloads.
This secret, stored in your .env.local file, adds security to your webhook. By including it as a query parameter in your webhook URL, you ensure only requests with the correct secret, presumably from MessageBird, are processed by your application.
The webhook sends data as application/x-www-form-urlencoded. Key fields include 'originator' (sender's number), 'payload' (the message content), 'recipient' (your virtual number), 'messageId', and 'createdDatetime'. Logging the full received data is recommended.
Use a strong, randomly generated secret as a query parameter in the URL. Verify this secret on your Next.js API route to ensure only MessageBird is triggering the webhook. Also, implement input sanitization, rate limiting, and possibly timestamp checks.
MessageBird typically handles long SMS (concatenated messages) automatically. You should receive the entire reassembled message in the 'payload' field of your webhook. Testing long messages is recommended.
Avoid using SQLite in production on serverless platforms like Vercel due to ephemeral file systems. Use a managed database service like Vercel Postgres, PlanetScale, or Supabase, especially if you need persistent data storage. For local development and testing, SQLite is convenient.
Prisma is an Object-Relational Mapper (ORM) that simplifies database interactions. It helps define your database schema, perform migrations, and access data easily within your API route handler. It's particularly useful for modeling and handling database relations for complex systems.
The guide recommends Prisma with SQLite for development and testing. For production, use a persistent, managed database solution. In your webhook handler, after validating the request, use Prisma Client to create a new database entry with the message details.
Implement thorough error handling and logging in your webhook handler. Catch potential errors, log them, and return appropriate status codes. Consider using a structured logging library like 'pino'. Critically, try to always return 200 OK to MessageBird quickly, even on errors, unless you specifically want MessageBird to retry.
Build a Next.js application that receives inbound SMS messages sent to your MessageBird virtual number. Create a webhook endpoint using Next.js API routes, configure MessageBird's Flow Builder to forward incoming messages, and deploy to Vercel.
Use this to build automated responders, two-way chat systems, notification triggers, or data collection services via text message.
What You'll Build
Technologies Used
System Architecture
(Ensure your publishing platform supports Mermaid diagram rendering)
Prerequisites
ngrokinstalled globally or available vianpxfor local testing.Note on Next.js Versions:
This guide is compatible with Next.js 14 and 15. If using Next.js 15, be aware that request-specific APIs (headers, cookies, params, searchParams) are now asynchronous, which may require adjustments if extending this implementation beyond the webhook handler shown.
Final Outcome
A deployed Next.js application with a publicly accessible API endpoint that logs incoming SMS messages sent to your configured MessageBird number. The endpoint will be secured with a basic shared secret.
1. Setting up the Project
Create a new Next.js application with the required configuration.
1.1 Create Next.js App:
Run this command in your terminal, replacing
messagebird-inbound-appwith your project name:Select these options when prompted:
src/directory? Yes1.2 Navigate to Project Directory:
cd messagebird-inbound-app1.3 Project Structure:
Your project structure with
src/directory and App Router:1.4 Environment Variables:
Create
.env.localin your project root to store sensitive configuration. Never commit this file to version control. It should be in.gitignoreby default.Generate a strong random secret:
node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))'Add the secret to
.env.local:Configuration Purpose:
create-next-app: Scaffolds a modern Next.js application with best practices..env.local: Stores environment-specific variables securely, keeping secrets out of your codebase.MESSAGEBIRD_WEBHOOK_SECRETverifies that webhook requests originate from MessageBird.2. Implementing the Webhook Handler
Create an API route in your Next.js application to receive webhook calls from MessageBird.
2.1 Create the API Route File:
Inside the
src/app/api/directory, create a new folder namedmessagebird-webhook. Inside this folder, create a file namedroute.js.Directory Structure:
src/app/api/messagebird-webhook/route.js2.2 Implement the Handler:
Paste the following code into
src/app/api/messagebird-webhook/route.js:// src/app/api/messagebird-webhook/route.js import { NextResponse } from 'next/server'; /** * Handles POST requests from MessageBird's Flow Builder for incoming SMS. * Expects form-encoded data. * @param {Request} request The incoming request object. * @returns {NextResponse} A response object. */ export async function POST(request) { console.log('Received request on /api/messagebird-webhook'); const secret = process.env.MESSAGEBIRD_WEBHOOK_SECRET; if (!secret) { console.error('MESSAGEBIRD_WEBHOOK_SECRET is not set in environment variables.'); // Return 200 OK to avoid MessageBird retries on config errors, // but log the server-side issue. return new NextResponse('Configuration error', { status: 200 }); } // --- Security Check --- // Get the secret from the query parameter (adjust if using headers) const url = new URL(request.url); const providedSecret = url.searchParams.get('secret'); if (providedSecret !== secret) { console.warn('Invalid or missing secret in webhook request.'); // Respond with 403 Forbidden if the secret is wrong return new NextResponse('Forbidden', { status: 403 }); } console.log('Webhook secret validated successfully.'); try { // MessageBird sends data as application/x-www-form-urlencoded const formData = await request.formData(); const messageData = Object.fromEntries(formData); // --- Log Incoming Message Data --- // Key fields based on MessageBird documentation for SMS webhooks: // - originator: The sender's phone number (e.g., +14155552671) // - recipient: Your MessageBird virtual number // - payload: The content of the SMS message // - messageId: Unique ID for the message // - createdDatetime: Timestamp of message creation // (Note: Field names can sometimes vary slightly based on MessageBird // configuration or message type. Logging the full object is recommended // to confirm the exact fields received in your specific setup. Common // fields include 'originator', 'payload', 'recipient', 'messageId', 'createdDatetime'.) console.log('Received MessageBird Payload:', JSON.stringify(messageData, null, 2)); const originator = messageData.originator; const payload = messageData.payload; const recipient = messageData.recipient; // Your virtual number const messageId = messageData.messageId; if (!originator || !payload) { console.warn('Missing required fields (originator or payload) in webhook data.'); // Return 400 Bad Request if essential data is missing return new NextResponse('Bad Request: Missing required fields', { status: 400 }); } // --- Process the Message (Example: Logging) --- // In a real application, you would: // 1. Store the message in a database. // 2. Trigger business logic (e.g., auto-reply, notification). // 3. Queue for further processing if needed. console.log(`Processing message ${messageId} from ${originator} to ${recipient}: "${payload}"`); // --- Respond to MessageBird --- // Important: Respond quickly with a 200 OK to acknowledge receipt. // MessageBird doesn't process the response body for SMS webhooks. // Avoid long-running tasks here; defer them if necessary. return new NextResponse('Webhook received successfully', { status: 200 }); } catch (error) { console.error('Error processing MessageBird webhook:', error); // Return 500 Internal Server Error for unexpected issues // Consider returning 200 OK even on errors if you don't want MessageBird // to retry, but ensure you have robust logging/alerting. return new NextResponse('Internal Server Error', { status: 500 }); } } /** * Handles GET requests (optional, e.g., for simple health checks). * @param {Request} request The incoming request object. * @returns {NextResponse} A response object. */ export async function GET(request) { console.log('Received GET request on /api/messagebird-webhook'); // You could add a health check here return new NextResponse('API endpoint is active. Use POST for webhooks.', { status: 200 }); }Code Explanation:
NextResponse: Used for sending responses from API routes.POSTFunction: This is the primary handler for incoming webhook requests from MessageBird, which uses thePOSTHTTP method for SMS webhooks.MESSAGEBIRD_WEBHOOK_SECRETis loaded correctly. If not, it logs an error but returns a200 OKto prevent MessageBird from endlessly retrying due to a server configuration issue. You must fix the environment variable setup if this occurs.secretquery parameter from the incoming request URL (e.g.,https://yourapp.com/api/messagebird-webhook?secret=your_secret).providedSecretwith the one stored in your environment variables (process.env.MESSAGEBIRD_WEBHOOK_SECRET).403 Forbiddenstatus, rejecting the request.application/x-www-form-urlencoded.request.formData()parses this into aFormDataobject, which we convert to a plain JavaScript object (messageData).originator,payload,recipient, etc.).originator,payload). If missing, it returns a400 Bad Request.200 OKresponse usingNextResponse. MessageBird expects this to confirm successful receipt. The response body is ignored by MessageBird for SMS webhooks.try...catchblock catches any unexpected errors during processing, logs them, and returns a500 Internal Server Error.GETFunction (Optional): A simple handler forGETrequests is included. This isn't used by MessageBird for SMS webhooks but can be useful for manually checking if the endpoint is reachable or for setting up simple external health checks.3. API Endpoint Details (Webhook)
While not a traditional user-facing API, the webhook endpoint acts as an API for MessageBird.
/api/messagebird-webhookPOST?secret=YOUR_SECRET).application/x-www-form-urlencodedoriginator: String (Sender's phone number in international format)payload: String (The SMS message content)recipient: String (Your MessageBird virtual number)messageId: String (Unique MessageBird ID)createdDatetime: String (ISO 8601 timestamp)200 OK(Empty body or simple text confirmation)403 Forbidden: Invalid or missingsecretquery parameter.400 Bad Request: Missing required fields (originatororpayload).500 Internal Server Error: Unexpected server-side error during processing.200 OK(with server-side error logged): IfMESSAGEBIRD_WEBHOOK_SECRETis not configured.Testing with
curl(Simulating MessageBird):You'll need your local server running (
npm run dev) andngrokexposing it (see Section 4). ReplaceYOUR_NGROK_URLandYOUR_SECRETaccordingly. Use a realistic past or generic date forcreatedDatetime.curl -X POST \ --header "Content-Type: application/x-www-form-urlencoded" \ -d "originator=+14155551234" \ -d "recipient=+12025550199" \ -d "payload=Hello from curl test" \ -d "messageId=mb-msg-id-123" \ -d "createdDatetime=2023-10-26T10:00:00Z" \ "YOUR_NGROK_URL/api/messagebird-webhook?secret=YOUR_SECRET"Note on
curlquoting:The command above uses double quotes for data fields and the URL. This generally works in bash/zsh. If you encounter issues in other shells or if your secret contains special shell characters, try putting single quotes around the entire URL:
'YOUR_NGROK_URL/api/messagebird-webhook?secret=YOUR_SECRET'.Check your terminal running
npm run devfor the log output from theroute.jshandler.4. Integrating with MessageBird
Configure MessageBird to send incoming SMS messages to your Next.js application.
4.1 Local Development Setup (ngrok):
MessageBird needs a publicly accessible URL to send webhooks to. During development, your local machine isn't typically accessible. Use
ngrokto create a secure tunnel.Start your Next.js dev server:
It usually runs on
http://localhost:3000.Start ngrok:
Open another terminal window and run:
Copy the ngrok URL:
ngrokwill display forwarding URLs. Copy thehttpsURL (e.g.,https://random-subdomain.ngrok-free.app). This is your temporary public URL.4.2 Configure MessageBird Flow Builder:
Log in to your MessageBird Dashboard.
Navigate to Flow Builder from the left-hand menu.
Click Create new flow > Create Custom Flow.
Give your flow a name (e.g., "Next.js Inbound SMS").
Choose SMS as the trigger. Click Next.
Configure the Trigger:
Add the Webhook Step:
Configure the HTTP Endpoint Step:
httpsURL (from step 4.1.3) and append the API route path and the secret query parameter:https://your-random-subdomain.ngrok-free.app/api/messagebird-webhook?secret=your_generated_secret_string_here(Replace the URL and secret with your actual values from.env.local)Publish the Flow:
Click the Publish button in the top-right corner. Confirm the publication.
Diagram of Flow Builder Setup:
(Ensure your publishing platform supports Mermaid diagram rendering)
Explanation of Environment Variables:
MESSAGEBIRD_WEBHOOK_SECRET(used in.env.localand later in Vercel): This secret string is added as a query parameter to the webhook URL configured in Flow Builder. The Next.js API route verifies this secret upon receiving a request, ensuring that only requests knowing the secret (presumably only MessageBird via your Flow Builder config) are processed.Testing Local Integration:
Send an SMS message from your phone to the MessageBird virtual number you configured in Flow Builder.
npm run dev. You should see theconsole.logoutput from yourroute.jsfile, including the "Webhook secret validated successfully" message and the "Received MessageBird Payload".ngrok. You should see an incomingPOSTrequest to/api/messagebird-webhookwith a200 OKresponse status.If it works, your local setup is correctly receiving messages!
5. Implementing Error Handling and Logging
The
route.jsfile already includes basic error handling and logging:MESSAGEBIRD_WEBHOOK_SECRET. Logs error server-side, returns200 OKto MessageBird.403 Forbidden.400 Bad Request.try...catchblock captures unexpected errors during processing. Logs error, returns500 Internal Server Error.console.log,console.warn, andconsole.errorfor different levels of information. The entire payload is logged for debugging.Improvements for Production:
Structured Logging:
Use a dedicated logging library like
pinofor structured JSON logs, which are easier to parse and analyze in log management systems.npm install pino pino-pretty # pino-pretty for devAdapt
route.jsto usepino.Error Tracking Services:
Integrate with services like Sentry or Bugsnag to capture, report, and analyze errors automatically.
Retry Mechanisms (Application Level):
If your processing logic involves external services that might fail temporarily, implement retry logic (e.g., using
async-retry) within your handler after sending the200 OKto MessageBird, or preferably by pushing the message to a queue (like Redis, RabbitMQ, or AWS SQS) for background processing with retries. Do not delay the200 OKresponse to MessageBird.Testing Error Scenarios:
Invalid Secret:
Send a
curlrequest (Section 3) with the wrong or nosecretparameter. Expect a403 Forbidden.Missing Payload:
Send a
curlrequest without theoriginatororpayloadform data fields. Expect a400 Bad Request.Simulate Internal Error:
Temporarily add
throw new Error('Simulated processing error');inside thetryblock before the finalreturn. Send a valid request. Expect a500 Internal Server Errorresponse and an error log in the console.6. Creating a Database Schema and Data Layer (Optional)
For most real-world applications, you'll want to store incoming messages. Let's add basic persistence using Prisma and SQLite.
⚠️ Critical Production Warning – SQLite on Vercel:
SQLite is NOT supported for production deployments on Vercel or other serverless platforms. This is a fundamental architectural incompatibility, not a configuration issue:
For Production on Vercel (as of 2025):
The following steps demonstrate SQLite for local development and learning purposes only. Replace with a proper database service before deploying to production.
6.1 Install Prisma:
6.2 Initialize Prisma:
This creates a
prismadirectory with aschema.prismafile and updates.env.localwith aDATABASE_URL.6.3 Define Schema:
Open
prisma/schema.prismaand define a model for incoming messages:6.4 Create Initial Migration:
This creates the SQLite database file (e.g.,
prisma/dev.dbbased on yourDATABASE_URL) and theIncomingMessagetable. Make sureprisma/dev.dbis added to your.gitignore.6.5 Implement Data Layer:
Create a utility function to instantiate and share the Prisma client instance.
// src/lib/prisma.js (Create this file) import { PrismaClient } from '@prisma/client'; let prisma; if (process.env.NODE_ENV === 'production') { // In production, always create a new instance // Note: Consider implications for serverless function reuse vs. connection limits prisma = new PrismaClient(); } else { // In development, reuse the instance across hot-reloads if (!global.prisma) { global.prisma = new PrismaClient({ // Optionally add logging for development // log: ['query', 'info', 'warn', 'error'], }); } prisma = global.prisma; } export default prisma;6.6 Update Webhook Handler to Save Message:
Modify
src/app/api/messagebird-webhook/route.js:// src/app/api/messagebird-webhook/route.js import { NextResponse } from 'next/server'; import prisma from '@/lib/prisma'; // Import prisma instance // Keep the existing POST function structure up to the security check export async function POST(request) { console.log('Received request on /api/messagebird-webhook'); const secret = process.env.MESSAGEBIRD_WEBHOOK_SECRET; if (!secret) { console.error('MESSAGEBIRD_WEBHOOK_SECRET is not set.'); return new NextResponse('Configuration error', { status: 200 }); } const url = new URL(request.url); const providedSecret = url.searchParams.get('secret'); if (providedSecret !== secret) { console.warn('Invalid or missing secret.'); return new NextResponse('Forbidden', { status: 403 }); } console.log('Webhook secret validated successfully.'); try { const formData = await request.formData(); const messageData = Object.fromEntries(formData); console.log('Received MessageBird Payload:', JSON.stringify(messageData, null, 2)); const originator = messageData.originator; const payload = messageData.payload; const recipient = messageData.recipient; const messageId = messageData.messageId; // MessageBird's ID const createdDatetime = messageData.createdDatetime; // MessageBird's timestamp if (!originator || !payload || !messageId) { // Add messageId check console.warn('Missing required fields (originator, payload, or messageId) in webhook data.'); return new NextResponse('Bad Request: Missing required fields', { status: 400 }); } // --- Save the Message to Database --- try { const newMessage = await prisma.incomingMessage.create({ data: { messageBirdId: messageId, originator: originator, recipient: recipient || 'unknown', // Handle cases where recipient might be missing payload: payload, messageBirdTs: createdDatetime ? new Date(createdDatetime) : null, // receivedAt is handled by @default(now()) in schema }, }); console.log(`Message ${newMessage.id} (MB ID: ${messageId}) saved to DB.`); } catch (dbError) { // Handle potential unique constraint errors if MessageBird retries // Prisma error code for unique constraint violation is P2002 if (dbError.code === 'P2002' && dbError.meta?.target?.includes('messageBirdId')) { console.warn(`Duplicate message received (MessageBird ID: ${messageId}). Ignoring.`); // Still return 200 OK as we've effectively processed/acknowledged it before return new NextResponse('Webhook received (duplicate ignored)', { status: 200 }); } else { console.error('Database error saving message:', dbError); // Decide if a DB error should cause a 500 or if you still return 200 OK // Returning 500 might cause MessageBird retries, potentially leading to more duplicates if the issue persists. // Returning 200 prevents retries but might lose data if the DB issue is temporary. // Robust solution: Queue message first, then process/save from queue. // For now, we return 500 to indicate a server-side problem beyond duplicates. return new NextResponse('Internal Server Error during DB operation', { status: 500 }); } } // --- Respond to MessageBird --- // Respond *after* attempting to save, unless it was a handled duplicate. return new NextResponse('Webhook received successfully', { status: 200 }); } catch (error) { // Catch errors outside the DB block (e.g., parsing formData) console.error('Error processing MessageBird webhook:', error); return new NextResponse('Internal Server Error', { status: 500 }); } } // Keep GET function export async function GET(request) { console.log('Received GET request on /api/messagebird-webhook'); return new NextResponse('API endpoint is active. Use POST for webhooks.', { status: 200 }); }Key Changes:
@/lib/prisma).try...catchblock specifically for the database operation (prisma.incomingMessage.create).prisma.incomingMessage.createto save the relevant data extracted frommessageData.P2002) specifically on themessageBirdIdfield. If it's a duplicate, log a warning and return200 OK.messageIdto the required fields check.200 OKresponse is now sent after the database operation attempt (unless it was a handled duplicate).Retest by sending an SMS. You should see the "saved to DB" log message in your development console. You can inspect the
prisma/dev.dbfile (using tools like DB Browser for SQLite) to see the saved record.7. Adding Security Features
Beyond the shared secret, consider these:
Input Sanitization:
While Prisma helps prevent SQL injection, if you use the
payloadelsewhere (e.g., displaying in a UI), sanitize it to prevent Cross-Site Scripting (XSS) attacks (e.g., using libraries likedompurify).Rate Limiting:
Implement rate limiting on the API route, especially if processing is resource-intensive, to prevent abuse or accidental loops. Libraries like
rate-limiter-flexibleor platform features (like Vercel's built-in IP rate limiting) can be used.Timestamp Verification (Optional):
Check the
createdDatetimefrom MessageBird against the current server time. Reject requests that are too old (e.g., > 5 minutes) to mitigate simple replay attacks, but be cautious about potential clock skew between MessageBird's servers and yours.HTTPS Enforcement:
Vercel deployments enforce HTTPS by default. Always ensure your
ngroktunnel useshttpsand your production webhook URL useshttps. Never use plainhttpfor webhooks handling sensitive data or secrets.Signature Verification (Advanced):
MessageBird can sign requests for certain webhook types (check their documentation for specifics, as it might not apply to basic Flow Builder SMS webhooks). If available and configurable for your setup, verifying a cryptographic signature (e.g., HMAC-SHA256) using a separate secret provides stronger assurance of authenticity than just a secret in the URL.
Testing Security:
curlor a similar tool to send requests without thesecret, with an incorrectsecret, or with malformed data to confirm your validation logic returns403 Forbiddenor400 Bad Requestappropriately.8. Handling Special Cases
Character Encoding:
SMS messages use specific encodings (
GSM-7orUCS-2). MessageBird typically handles the decoding and provides the messagepayloadas a standard UTF-8 string in the webhook. Be aware of this if your application needs to interact with external systems expecting specific SMS encodings.Concatenated Messages (Long SMS):
MessageBird usually reassembles long SMS messages (split into multiple parts over the air) before triggering the webhook. The
payloadyou receive should contain the full message content. Test with messages longer than 160 characters (GSM-7) or 70 characters (UCS-2) to confirm this behavior.Message Ordering:
Webhooks are delivered over HTTP and network conditions or MessageBird retries can mean they don't necessarily arrive in the exact order the original SMS messages were sent or received by MessageBird. If strict ordering is critical for your application logic, use timestamps (
createdDatetimefrom MessageBird or yourreceivedAttimestamp) to order messages during processing or retrieval.Non-Text Content (MMS):
This guide focuses purely on standard SMS text messages. If you need to receive and process MMS messages (containing images, videos, etc.), the webhook payload structure and potentially the Flow Builder configuration will differ significantly. Consult MessageBird's specific MMS documentation.
Internationalization:
Phone numbers (
originator,recipient) are generally provided in the standard E.164 format (e.g.,+14155552671). Ensure your system stores and processes these numbers correctly. If your application logic depends on the content (payload) of the message (e.g., language detection, keyword analysis), consider internationalization and localization requirements.9. Implementing Performance Optimizations
For a simple webhook receiver primarily logging or storing data, performance is usually manageable unless facing very high message volumes. Key considerations include:
Vercel Serverless Function Limits (as of 2025):
Understanding platform constraints helps you design within boundaries and avoid runtime failures:
Source: Vercel official documentation (https://vercel.com/docs/functions/limitations, verified October 2025)
Fast Responses:
The absolute most critical performance factor is responding with
200 OKto MessageBird as quickly as possible (ideally within 1-2 seconds). Defer any potentially slow or unreliable operations (like complex database queries, external API calls, sending replies) until after the response has been sent.Asynchronous Processing / Queues:
For tasks that take longer than a few hundred milliseconds or involve external dependencies, push the incoming message data (or just an ID) onto a message queue (e.g., Redis, RabbitMQ, AWS SQS, Vercel KV Queue) immediately after validation. Have separate background workers process messages from the queue. This decouples message reception from processing, ensuring fast webhook responses.
Database Connection Management:
In serverless environments, manage database connections efficiently. Use connection pooling (Prisma handles this) and be mindful of connection limits, especially on free tiers of managed databases. The Prisma client instantiation pattern shown earlier helps reuse connections in development but creates new ones in production per function invocation, which is generally correct for serverless but needs monitoring under load.
Caching:
If processing involves frequently accessed data that doesn't change often, implement caching (e.g., using Redis, Vercel KV, or in-memory caches with appropriate invalidation) to speed up lookups.
Code Optimization:
Profile your code (using
console.time/console.timeEndor dedicated profiling tools) to identify bottlenecks within the handler if performance issues arise. Optimize algorithms and data structures.Platform Scaling:
Leverage the auto-scaling capabilities of your deployment platform (Vercel). Ensure your database and any external services can also handle the potential load.
Testing Performance:
k6orartillery.ioto simulate high volumes of concurrent webhook requests to your deployed endpoint (not justngrok).