code examples

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

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.

.env.local

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

mermaid
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:

bash
npx create-next-app@latest messagebird-inbound-app

Select these options when prompted:

  • Would you like to use TypeScript? No (for simplicity, but adaptable to TS)
  • Would you like to use ESLint? Yes
  • Would you like to use Tailwind CSS? No (not needed for this backend focus)
  • Would you like to use src/ directory? Yes
  • Would you like to use App Router? Yes (Recommended)
  • Would you like to customize the default import alias? No

1.2 Navigate to Project Directory:

bash
cd messagebird-inbound-app

1.3 Project Structure:

Your project structure with src/ directory and App Router:

text
messagebird-inbound-app/
├── src/
│   ├── app/
│   │   ├── api/                  # API routes live here
│   │   │   └── messagebird-webhook/
│   │   │       └── route.js      # Our webhook handler
│   │   ├── globals.css
│   │   ├── layout.js
│   │   └── page.js
├── public/
├── .env.local                  # Environment variables (create this file)
├── .gitignore
├── next.config.mjs
├── package.json
└── README.md

1.4 Environment Variables:

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.

Generate a strong random secret:

bash
node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))'

Add the secret to .env.local:

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

Directory Structure: src/app/api/messagebird-webhook/route.js

2.2 Implement the Handler:

Paste the following code into src/app/api/messagebird-webhook/route.js:

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

  1. Import NextResponse: Used for sending responses from API routes.
  2. POST Function: This is the primary handler for incoming webhook requests from MessageBird, which uses the POST HTTP method for SMS webhooks.
  3. 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.
  4. 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.
  5. 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).
  6. 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.).
  7. Data Validation: Checks for the presence of essential fields (originator, payload). If missing, it returns a 400 Bad Request.
  8. 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.
  9. 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.
  10. Error Handling: A try...catch block catches any unexpected errors during processing, logs them, and returns a 500 Internal Server Error.
  11. 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)
    • payload: String (The SMS message content)
    • recipient: String (Your MessageBird virtual number)
    • messageId: String (Unique MessageBird ID)
    • createdDatetime: String (ISO 8601 timestamp)
    • (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.

bash
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 curl quoting:

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.

  1. Start your Next.js dev server:

    bash
    npm run dev

    It usually runs on http://localhost:3000.

  2. Start ngrok:

    Open another terminal window and run:

    bash
    ngrok http 3000
  3. 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:

  1. Log in to your MessageBird Dashboard.

  2. Navigate to Flow Builder from the left-hand menu.

  3. Click Create new flow > Create Custom Flow.

  4. Give your flow a name (e.g., "Next.js Inbound SMS").

  5. Choose SMS as the trigger. Click Next.

  6. Configure the Trigger:

    • Click the "SMS" trigger step.
    • Select the MessageBird virtual Number(s) you want to use for receiving messages.
    • Click Save.
  7. Add the Webhook Step:

    • Click the + icon below the SMS trigger.
    • Search for and select the Call HTTP endpoint with SMS step.
  8. 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.
  9. Publish the Flow:

    Click the Publish button in the top-right corner. Confirm the publication.

Diagram of Flow Builder Setup:

mermaid
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.
  • Security Errors: Checks for invalid/missing secret. Logs warning, returns 403 Forbidden.
  • 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.

6.1 Install Prisma:

bash
npm install prisma --save-dev
npm install @prisma/client

6.2 Initialize Prisma:

bash
npx prisma init --datasource-provider sqlite

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 = 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:

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

  1. Imported the Prisma client instance (@/lib/prisma).
  2. Added a try...catch block specifically for the database operation (prisma.incomingMessage.create).
  3. Used prisma.incomingMessage.create to save the relevant data extracted from messageData.
  4. 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.
  5. Added messageId to the required fields check.
  6. 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

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