Track the delivery status of SMS and MMS messages sent via Twilio directly within your Next.js application. This guide provides a complete walkthrough for setting up a webhook endpoint to receive status updates, storing them in a database, and handling common scenarios.
Knowing the real-time status of your sent messages – whether they were delivered, failed, or are still in transit – is crucial for applications relying on SMS for notifications, alerts, or two-factor authentication. Implementing Twilio's status callbacks provides this visibility, enabling better error handling, logging, and user experience.
This guide will walk you through building a Next.js application that sends an SMS via Twilio and uses a dedicated API route to listen for and record status updates pushed by Twilio. We'll use Prisma for database interaction and cover essential aspects like security, error handling, and deployment.
Project Overview and Goals
What We'll Build:
- A Next.js application with an API route (
/api/twilio/status
) acting as a webhook endpoint for Twilio status callbacks. - A mechanism to send an SMS message via the Twilio API, specifying our webhook endpoint URL in the
statusCallback
parameter. - A database schema (using Prisma) to store incoming message status updates (
MessageSid
,MessageStatus
,ErrorCode
, etc.). - Secure handling of Twilio webhooks using signature validation.
Problem Solved:
Gain real-time visibility into the delivery lifecycle of outbound Twilio messages, enabling robust tracking, debugging, and potentially triggering follow-up actions based on message status (e.g., retrying failed messages, logging delivery confirmations).
Technologies Used:
- Next.js: React framework for building the frontend and API routes. Chosen for its developer experience, performance features, and integrated API capabilities.
- Twilio: Communications Platform as a Service (CPaaS) for sending SMS/MMS. Chosen for its robust APIs and webhook features.
- Prisma: Next-generation ORM for Node.js and TypeScript. Chosen for its type safety, developer-friendly schema management, and migrations.
- Node.js: JavaScript runtime environment.
- (Optional)
ngrok
: For exposing the local development server to the internet, enabling Twilio to reach the webhook endpoint during development.
System Architecture:
The system operates as follows:
- A user or client triggers an action in the Next.js application.
- The Next.js API sends an SMS request to the Twilio API, including a
StatusCallback
URL pointing back to our application. - Twilio sends the SMS towards the end user via the carrier network.
- As the message status changes (e.g., sent, delivered, failed), Twilio sends an HTTP POST request containing the status update to the specified
StatusCallback
URL (which must be publicly accessible, potentially viangrok
during development or the deployed application URL in production). - The public URL forwards the request to the designated Next.js API route (e.g.,
/api/twilio/status
). - The API route validates the incoming request's signature to ensure it's genuinely from Twilio.
- If valid, the route processes the status update and stores relevant information (like
MessageSid
,MessageStatus
) in the database using Prisma. - The API route sends a
200 OK
HTTP response back to Twilio to acknowledge receipt of the status update.
Prerequisites:
- Node.js (LTS version recommended) and npm/yarn installed.
- A Twilio account with an active phone number or Messaging Service SID. Find your Account SID and Auth Token in the Twilio Console.
- Basic understanding of Next.js, React, and asynchronous JavaScript.
- A tool to expose your local development server (like
ngrok
) if testing locally. - A database supported by Prisma (e.g., PostgreSQL, MySQL, SQLite). This guide will use SQLite for simplicity during development.
Final Outcome:
A functional Next.js application capable of sending SMS messages via Twilio and reliably receiving, validating, and storing delivery status updates pushed by Twilio to a secure webhook endpoint.
Setting up the Project
Let's initialize our Next.js project and install necessary dependencies.
-
Create a new Next.js App: Open your terminal and run:
npx create-next-app@latest twilio-status-callback-guide cd twilio-status-callback-guide
Choose your preferred settings (TypeScript recommended). This guide assumes you are using the
app
directory structure. -
Install Dependencies: We need the Twilio Node.js helper library and Prisma.
npm install twilio @prisma/client npm install prisma --save-dev
twilio
: Official library for interacting with the Twilio API.@prisma/client
: Prisma's database client.prisma
: Prisma's command-line tool for migrations, schema management, etc.
-
Initialize Prisma: Set up Prisma in your project. This creates a
prisma
directory with aschema.prisma
file and a.env
file for environment variables.npx prisma init --datasource-provider sqlite
--datasource-provider sqlite
: We're using SQLite for simplicity here. Change this (e.g.,postgresql
,mysql
) if you prefer a different database and update theDATABASE_URL
accordingly.
-
Configure Environment Variables: Open the
.env
file created by Prisma and add your Twilio credentials and other necessary variables. Never commit this file to version control if it contains secrets.# .env # Database # The default SQLite URL created by `prisma init` DATABASE_URL=""file:./dev.db"" # Twilio Credentials (Get from https://www.twilio.com/console) TWILIO_ACCOUNT_SID=""ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"" TWILIO_AUTH_TOKEN=""your_auth_token_xxxxxxxxxxxxxx"" # Also used as Webhook Secret # Your Twilio Phone Number or Messaging Service SID TWILIO_PHONE_NUMBER_OR_MSG_SID=""+15551234567"" # Or MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # The base URL where your app is running (for constructing callback URL) # During development with ngrok, this will be your ngrok URL # In production, this will be your deployed app's URL NEXT_PUBLIC_APP_BASE_URL=""http://localhost:3000"" # Update with ngrok/prod URL later
DATABASE_URL
: Connection string for your database. Prisma set this up for SQLite.TWILIO_ACCOUNT_SID
: Your unique Twilio account identifier.TWILIO_AUTH_TOKEN
: Your secret Twilio token. Crucially, Twilio uses this same token to sign webhook requests, so we'll use it as our webhook validation secret.TWILIO_PHONE_NUMBER_OR_MSG_SID
: The 'From' number for sending messages, or the SID of a configured Messaging Service.NEXT_PUBLIC_APP_BASE_URL
: The publicly accessible base URL of your application. While prefixed withNEXT_PUBLIC_
(allowing potential client-side access), it's used server-side in this guide (in thesend-sms
route) to construct the fullstatusCallback
URL. If client-side access is never needed, a non-prefixed variable likeAPP_BASE_URL
could be used. Update this when using ngrok or deploying.
-
Project Structure: Your basic structure will look something like this (using
app
router):twilio-status-callback-guide/ ├── app/ │ ├── api/ │ │ ├── twilio/ │ │ │ └── status/ │ │ │ └── route.ts # Our webhook handler │ │ └── send-sms/ # API route to send SMS │ │ └── route.ts │ ├── lib/ │ │ └── prisma.ts # Prisma client instance │ └── page.tsx # Simple UI to send a message (optional) ├── prisma/ │ ├── schema.prisma # Database schema definition │ └── dev.db # SQLite database file (after migration) ├── .env # Environment variables (DO NOT COMMIT) ├── next.config.mjs ├── package.json └── tsconfig.json
Creating a Database Schema and Data Layer
We need a place to store the incoming status updates.
-
Define the Prisma Schema: Open
prisma/schema.prisma
and define a model to log the message statuses.// prisma/schema.prisma generator client { provider = ""prisma-client-js"" } datasource db { provider = ""sqlite"" // Or your chosen provider url = env(""DATABASE_URL"") } model MessageStatusLog { id Int @id @default(autoincrement()) messageSid String @unique // Twilio's Message SID status String // e.g., 'queued', 'sent', 'delivered', 'failed' errorCode String? // Twilio error code if status is 'failed' or 'undelivered' rawPayload Json // Store the full webhook payload for debugging/future use timestamp DateTime @default(now()) // When the log entry was created }
- We store the
messageSid
(which should be unique per message), thestatus
, an optionalerrorCode
, the fullrawPayload
as JSON for auditing, and atimestamp
.
- We store the
-
Apply the Schema to the Database: Run the Prisma command to create the database file (for SQLite) and the
MessageStatusLog
table.npx prisma db push
- This command synchronizes your database schema with your
schema.prisma
definition. For production workflows, you'd typically useprisma migrate dev
andprisma migrate deploy
.
- This command synchronizes your database schema with your
-
Create a Prisma Client Instance: Create a reusable Prisma client instance. Create
app/lib/prisma.ts
:// app/lib/prisma.ts import { PrismaClient } from '@prisma/client'; let prisma: PrismaClient; declare global { // allow global `var` declarations // eslint-disable-next-line no-var var prisma: PrismaClient | undefined; } if (process.env.NODE_ENV === 'production') { prisma = new PrismaClient(); } else { // Ensure the prisma instance is re-used during development // See: https://pris.ly/d/help/next-js-best-practices if (!global.prisma) { global.prisma = new PrismaClient({ // Optional: Log Prisma queries during development // log: ['query', 'info', 'warn', 'error'], }); } prisma = global.prisma; } export default prisma;
- This pattern prevents creating multiple Prisma client instances during hot-reloading in development.
Implementing the Core Functionality (Webhook API Route)
Now, let's create the API route that will receive the POST
requests from Twilio.
-
Create the API Route File: Create the file
app/api/twilio/status/route.ts
. -
Implement the Webhook Handler:
// app/api/twilio/status/route.ts import { NextRequest, NextResponse } from 'next/server'; import twilio from 'twilio'; import prisma from '@/app/lib/prisma'; // Adjust path if needed import { URLSearchParams } from 'url'; // Node.js native URLSearchParams // Fetch environment variables const twilioAuthToken = process.env.TWILIO_AUTH_TOKEN || ''; export async function POST(req: NextRequest) { if (!twilioAuthToken) { console.error('Twilio Auth Token not configured in environment variables.'); return new NextResponse('Configuration error: Twilio Auth Token missing.', { status: 500 }); } // Construct the full URL for validation // Use req.url which includes the full path Next.js received const fullUrl = req.url; if (!fullUrl) { console.error('Could not determine request URL.'); return new NextResponse('Internal Server Error: Cannot determine request URL.', { status: 500 }); } // Get Twilio signature from headers const signature = req.headers.get('x-twilio-signature') || ''; try { // Get the raw body text const rawBody = await req.text(); // Parse the raw body into parameters using URLSearchParams const params = new URLSearchParams(rawBody); const paramsObject = Object.fromEntries(params.entries()); // Validate the request signature const isValid = twilio.validateRequest( twilioAuthToken, signature, fullUrl, paramsObject // Pass the parsed parameters object ); if (!isValid) { console.warn('Invalid Twilio signature received.'); return new NextResponse('Invalid Twilio Signature', { status: 403 }); } // --- Signature is valid, process the status update --- const messageSid = params.get('MessageSid'); const messageStatus = params.get('MessageStatus'); const errorCode = params.get('ErrorCode') || null; // Get error code if present if (!messageSid || !messageStatus) { console.warn('Webhook received without MessageSid or MessageStatus.'); return new NextResponse('Missing required parameters', { status: 400 }); } console.log(`Received status update for SID: ${messageSid}, Status: ${messageStatus}, ErrorCode: ${errorCode}`); // Store the update in the database try { await prisma.messageStatusLog.create({ data: { messageSid: messageSid, status: messageStatus, errorCode: errorCode, rawPayload: paramsObject, // Store the full payload as JSON }, }); console.log(`Successfully logged status for SID: ${messageSid}`); } catch (dbError: any) { // Handle potential unique constraint violation if reprocessing if (dbError.code === 'P2002' && dbError.meta?.target?.includes('messageSid')) { console.warn(`Duplicate status update ignored for SID: ${messageSid}, Status: ${messageStatus}`); // Still return 200 OK to Twilio, as we've acknowledged it return new NextResponse('Status update acknowledged (duplicate)', { status: 200 }); } // Log other DB errors console.error(`Database error logging status for SID ${messageSid}:`, dbError); // Return 500 so Twilio might retry (depending on the error) return new NextResponse('Database error', { status: 500 }); } // Respond to Twilio with 200 OK // An empty response body is sufficient return new NextResponse(null, { status: 200 }); } catch (error: any) { console.error('Error processing Twilio status webhook:', error); // Return 500 to indicate a server error return new NextResponse('Webhook handler error', { status: 500 }); } }
Explanation:
- Environment Variables: Retrieve the
TWILIO_AUTH_TOKEN
. - Access Raw Body: In Next.js App Router Route Handlers (
route.ts
), the request body forPOST
requests is not automatically parsed. We access the raw body usingreq.text()
, which is needed for signature validation. - Get Signature & URL: Extract the
x-twilio-signature
header and get the full request URL (req.url
). Twilio uses the full URL in its signature calculation. - Parse Body: Parse the
application/x-www-form-urlencoded
string fromreq.text()
into key-value pairs usingURLSearchParams
and convert it to an object. - Validate Signature: Use
twilio.validateRequest
with your Auth Token (acting as the secret), the signature, the full URL, and the parsed parameters object. This is the most critical security step. - Handle Invalid Signature: If validation fails, log a warning and return
403 Forbidden
. - Extract Data: If valid, extract
MessageSid
,MessageStatus
, andErrorCode
from the parsed parameters. - Store in Database: Use the Prisma client (
prisma.messageStatusLog.create
) to save the information and the full payload (paramsObject
). - Handle DB Errors: Includes handling for unique constraint violations (
P2002
) and logs other DB errors. - Respond 200 OK: Send an HTTP
200 OK
response back to Twilio to acknowledge receipt. No response body is needed. - General Error Handling: A top-level
try...catch
logs unexpected errors and returns a500 Internal Server Error
.
- Environment Variables: Retrieve the
Integrating with Twilio (Sending the Message)
Now we need a way to send a message and tell Twilio where to send status updates.
-
Create a Simple UI (Optional but helpful for testing): Let's add a basic button in
app/page.tsx
to trigger sending an SMS. Note that the example UI code includes a placeholder phone number (+15558675309
) which must be replaced with a valid number you can send messages to for testing.// app/page.tsx ""use client""; // This component needs to be a Client Component for onClick import { useState } from 'react'; export default function HomePage() { const [messageSid, setMessageSid] = useState<string | null>(null); const [status, setStatus] = useState<string>(''); const [error, setError] = useState<string | null>(null); const [isLoading, setIsLoading] = useState<boolean>(false); const handleSendMessage = async () => { setIsLoading(true); setMessageSid(null); setStatus('Sending...'); setError(null); try { const response = await fetch('/api/send-sms', { // We'll create this API route next method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ // IMPORTANT: Replace with a valid E.164 formatted phone number you can test with to: '+15558675309', body: `Hello from Next.js! Testing status callbacks. [${Date.now()}]`, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to send message'); } setMessageSid(data.sid); setStatus(`Message queued (SID: ${data.sid}). Check logs/DB for status updates.`); } catch (err: any) { console.error(""Send message error:"", err); setError(err.message || 'An unknown error occurred.'); setStatus('Failed to initiate sending.'); } finally { setIsLoading(false); } }; return ( <main style={{ padding: '2rem' }}> <h1>Twilio Status Callback Test</h1> <button onClick={handleSendMessage} disabled={isLoading}> {isLoading ? 'Sending...' : 'Send Test SMS'} </button> <div style={{ marginTop: '1rem' }}> <p><strong>Status:</strong> {status}</p> {messageSid && <p><strong>Message SID:</strong> {messageSid}</p>} {error && <p style={{ color: 'red' }}><strong>Error:</strong> {error}</p>} </div> <p style={{ marginTop: '2rem'_ fontStyle: 'italic' }}> After sending, check your application logs and database (`MessageStatusLog` table) for status updates received via the webhook. If testing locally, ensure ngrok is running and pointing to your development server. </p> </main> ); }
-
Create the Sending API Route: Create
app/api/send-sms/route.ts
to handle the actual Twilio API call.// app/api/send-sms/route.ts import { NextRequest, NextResponse } from 'next/server'; import twilio from 'twilio'; // Fetch environment variables securely on the server const accountSid = process.env.TWILIO_ACCOUNT_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; const fromNumber = process.env.TWILIO_PHONE_NUMBER_OR_MSG_SID; // Use the potentially public base URL variable from env const appBaseUrl = process.env.NEXT_PUBLIC_APP_BASE_URL; if (!accountSid || !authToken || !fromNumber || !appBaseUrl) { console.error(""Twilio credentials or App Base URL are not configured in environment variables.""); // Do not expose which specific variable is missing to the client // Throwing an error here will result in a 500 response handled by Next.js throw new Error(""Server configuration error.""); } const client = twilio(accountSid, authToken); export async function POST(req: NextRequest) { try { const body = await req.json(); const to = body.to; const messageBody = body.body; if (!to || !messageBody) { return NextResponse.json({ error: 'Missing ""to"" or ""body"" parameter' }, { status: 400 }); } // Construct the absolute StatusCallback URL const statusCallbackUrl = `${appBaseUrl}/api/twilio/status`; console.log(`Sending SMS to: ${to}`); console.log(`Using StatusCallback URL: ${statusCallbackUrl}`); const message = await client.messages.create({ body: messageBody, from: fromNumber, to: to, // THIS IS THE KEY: Tell Twilio where to send status updates statusCallback: statusCallbackUrl, // Optional: Specify which events trigger a callback // statusCallbackEvent: ['sent', 'failed', 'delivered', 'undelivered'], // Default includes all terminal states }); console.log('Message sent successfully, SID:', message.sid); return NextResponse.json({ sid: message.sid, status: message.status }); } catch (error: any) { console.error('Error sending Twilio message:', error); // Avoid sending detailed internal errors to the client let errorMessage = 'Failed to send message.'; // Example check for specific Twilio error // if (error.code === 21211) { // Invalid 'To' number // errorMessage = ""Invalid recipient phone number.""; // } // Return a generic error message, potentially logging the specific details server-side return NextResponse.json({ error: errorMessage, details: error.message }, { status: 500 }); } }
Explanation:
- Initializes the Twilio client using environment variables.
- Defines a
POST
handler expecting JSON withto
andbody
. - Constructs the
statusCallback
URL usingNEXT_PUBLIC_APP_BASE_URL
and the webhook route path (/api/twilio/status
). - Calls
client.messages.create
, passing recipient, sender, body, and the crucialstatusCallback
URL. - Returns the
message.sid
and initialstatus
(e.g.,queued
). - Includes basic error handling.
-
Local Development with
ngrok
: Twilio needs to sendPOST
requests to your application, so your local server (http://localhost:3000
) must be publicly accessible.- Install ngrok: Follow instructions at ngrok.com.
- Run ngrok: Open a new terminal window (keep your Next.js dev server running) and execute:
ngrok http 3000
- Get Public URL:
ngrok
will display a ""Forwarding"" URL (e.g.,https://<random-subdomain>.ngrok-free.app
). This is your public URL. - Update
.env
: Copy thehttps://...
ngrok URL and updateNEXT_PUBLIC_APP_BASE_URL
in your.env
file:# .env (Example update) NEXT_PUBLIC_APP_BASE_URL=""https://<random-subdomain>.ngrok-free.app""
- Restart Next.js Dev Server: Stop (
Ctrl+C
) and restart (npm run dev
) your Next.js server to load the updated environment variable.
Now, when sending a message:
- The
statusCallback
URL sent to Twilio will use your publicngrok
URL. - Twilio can send
POST
requests to this URL. ngrok
forwards these requests tohttp://localhost:3000/api/twilio/status
.- Monitor requests in the
ngrok
terminal and your Next.js logs.
Implementing Proper Error Handling, Logging, and Retry Mechanisms
- Error Handling (Webhook):
- The
/api/twilio/status
route usestry...catch
for validation, DB operations, and general processing. - Returning non-
200
status codes (403
,500
) signals issues to Twilio. Twilio typically retries on5xx
errors. - Specific DB error handling (e.g.,
P2002
for duplicates) prevents noise if Twilio resends data.
- The
- Logging:
- Use
console.log
,console.warn
,console.error
for basic logging. In production, adopt a structured logging library (e.g.,pino
,winston
) and send logs to an aggregation service (e.g., Datadog, Logtail, Axiom). - Log key events: Webhook receipt, validation status, data extraction, DB operations. Include
MessageSid
for correlation.
- Use
- Retry Mechanisms (Twilio's Side):
- Twilio automatically retries status callbacks if your endpoint doesn't respond
200 OK
within 15 seconds or returns a5xx
error, using exponential backoff. - Ensure your endpoint is idempotent: processing the same status update multiple times shouldn't cause errors. Our DB logging handles duplicates gracefully by ignoring them and returning
200 OK
.
- Twilio automatically retries status callbacks if your endpoint doesn't respond
- Testing Error Scenarios:
- Invalid Signature: Temporarily change
TWILIO_AUTH_TOKEN
in.env
, restart, send a message. Webhook validation should fail (403). Alternatively, usecurl
or Postman to send a request without a valid signature. - DB Error: Introduce a temporary error in the
prisma.messageStatusLog.create
call (e.g., misspell a field) to observe the500
response and logs. - Missing Parameters: Use a tool like Postman to send a validly signed request but omit
MessageSid
. Verify the400 Bad Request
response.
- Invalid Signature: Temporarily change
Adding Security Features
- Webhook Signature Validation: Primary security mechanism, implemented in
/api/twilio/status
usingtwilio.validateRequest
. Prevents fake status updates. - HTTPS: Always use HTTPS for your webhook URL in production.
ngrok
provides HTTPS locally. Deployment platforms (Vercel, Netlify) enforce HTTPS. - Environment Variables: Secure Twilio credentials (
TWILIO_ACCOUNT_SID
,TWILIO_AUTH_TOKEN
) using environment variables. Do not hardcode or commit them. Use platform secret management in production. - Input Validation: Basic checks (e.g., existence of
MessageSid
,MessageStatus
) are implemented as good practice. - Rate Limiting (Optional): Consider rate limiting (e.g.,
upstash/ratelimit
) for high-traffic public endpoints to prevent abuse, though less critical for specific webhooks unless targeted attacks are a concern.
Handling Special Cases Relevant to the Domain
- Webhook Order: Twilio does not guarantee status callbacks arrive in the order events occurred. A
sent
callback might arrive afterdelivered
.- Solution: Consider event timestamps or logical status progression if updating a single record per message. Our approach of creating a new log entry for each status naturally preserves the arrival order and the status received at that time. The
timestamp
field inMessageStatusLog
helps reconstruct history.
- Solution: Consider event timestamps or logical status progression if updating a single record per message. Our approach of creating a new log entry for each status naturally preserves the arrival order and the status received at that time. The
- Error Codes: When
MessageStatus
isfailed
orundelivered
, check theErrorCode
field. Consult the Twilio Error Dictionary for meanings (e.g.,30003
- Unreachable,30007
- Carrier Violation). Log these codes. - Different Channels: Payloads for channels like WhatsApp may have extra fields. Storing the
rawPayload
as JSON preserves this data. - DLR
RawDlrDoneDate
: SMS/MMS callbacks might includeRawDlrDoneDate
(YYMMDDhhmm) for carrier's final status timestamp. Parse and store if needed.
Implementing Performance Optimizations
- Database Indexing: Prisma automatically indexes
@id
. The@unique
constraint onmessageSid
also creates an index, ensuring efficient lookups/checks. - Async Operations: The webhook handler uses
async/await
correctly, avoiding blocking operations. - Webhook Response Time: Respond to Twilio quickly (within 15 seconds) with
200 OK
after validation. Perform time-consuming tasks after responding or asynchronously (background jobs/queues) if needed. The current implementation (validate, DB write, respond) is typically fast enough. - Caching: Generally not applicable for receiving webhook status updates.
Adding Monitoring, Observability, and Analytics
- Health Checks: Create a simple health check endpoint (e.g.,
/api/health
) returning200 OK
. - Logging: Use structured logging and send logs to a centralized platform (see Section 5).
- Error Tracking: Integrate a service like Sentry or Bugsnag to capture unhandled exceptions automatically.
npm install @sentry/nextjs # Follow Sentry's Next.js setup guide
- Metrics & Dashboards:
- Log key metrics: Webhooks received, validation success/fail, DB write success/error, processing time.
- Visualize metrics using your logging/monitoring platform.
- Track
MessageStatus
distribution (delivered vs. failed) and commonErrorCode
values.
- Alerting: Configure alerts for:
- High rate of
5xx
or403
errors from the webhook. - Spikes in specific
ErrorCode
values. - Health check failures.
- High rate of
Troubleshooting and Caveats
- Callbacks Not Received:
- Public URL? Verify
ngrok
or deployment URL is correct and accessible. - Correct
statusCallback
URL? Check the URL used inmessages.create
(includinghttps://
, domain, path/api/twilio/status
). Verify logs from the sending API route. - Server Running? Ensure the Next.js server is running and reachable.
- Twilio Debugger: Check Monitor > Logs > Error logs in the Twilio Console. Filter for webhook failures related to your URL. Look for HTTP response codes Twilio received from your endpoint.
- Firewall/Network Issues: Ensure no firewalls are blocking Twilio's IPs (less common with standard hosting/ngrok).
- Public URL? Verify
- Signature Validation Fails (403):
- Correct
TWILIO_AUTH_TOKEN
? Ensure theTWILIO_AUTH_TOKEN
in your.env
exactly matches the Auth Token shown in your Twilio Console. Remember to restart the server after.env
changes. - Correct URL used in validation? Ensure
twilio.validateRequest
uses the exact URL Twilio called, including protocol (https://
) and any query parameters (though unlikely for status webhooks).req.url
in Next.js App Router should provide this. - Correct Parameters used in validation? Ensure the parsed key-value pairs from the raw request body (
paramsObject
in the example) are passed tovalidateRequest
. Do not pass the raw string or a re-serialized version.
- Correct
- Database Errors:
- Schema Mismatch? Ensure
prisma/schema.prisma
matches the actual database structure (npx prisma db push
or migrations applied). - Connection Issues? Verify
DATABASE_URL
is correct and the database is reachable. - Permissions? Ensure the application has write permissions to the database.
- Unique Constraint (
P2002
)? This is expected if Twilio retries and sends the exact sameMessageSid
. The code handles this by logging a warning and returning200 OK
. If happening unexpectedly, investigate why duplicate SIDs are being processed.
- Schema Mismatch? Ensure
- Idempotency: If your processing logic beyond simple logging is complex, ensure it can handle receiving the same status update multiple times without causing incorrect side effects.
ngrok
Session Expiry: Freengrok
sessions expire and provide a new URL each time you restartngrok
. Remember to updateNEXT_PUBLIC_APP_BASE_URL
and restart your Next.js server whenever thengrok
URL changes. Consider a paidngrok
plan for stable subdomains if needed frequently during development.