Frequently Asked Questions
Set up a webhook endpoint in your Next.js application using an API route. This endpoint will receive real-time status updates from Twilio as your messages are sent, delivered, or encounter issues. The guide recommends using Prisma to store these updates in a database for reliable tracking.
A Twilio status callback is an HTTP POST request sent by Twilio to your application. It contains information about the delivery status of your SMS/MMS messages, such as 'queued', 'sent', 'delivered', or 'failed'. This allows for real-time monitoring and handling of message delivery events.
Prisma is a next-generation ORM that simplifies database interactions in Node.js and TypeScript. It's used in this guide to store the message status updates received from Twilio, ensuring type safety and efficient database operations. Prisma's schema management and migrations make it easy to manage database changes.
ngrok is essential for local development when your Next.js application isn't publicly accessible. ngrok creates a public tunnel to your localhost, allowing Twilio to reach your webhook endpoint during testing. You'll need to update your NEXT_PUBLIC_APP_BASE_URL
environment variable with the ngrok URL.
Yes, Prisma supports various databases like PostgreSQL, MySQL, and SQLite. The guide uses SQLite for simplicity, but you can change the datasource-provider
in your schema.prisma
and update the DATABASE_URL
environment variable accordingly. Choose the database best suited for your project's needs.
The most important security measure is validating the Twilio request signature. The provided code demonstrates how to use the twilio.validateRequest
function to verify that incoming requests originate from Twilio and haven't been tampered with. Always use HTTPS in production.
The statusCallback
parameter in the Twilio Messages API is the URL where Twilio will send status updates about your message. It should point to your Next.js API route, e.g., /api/twilio/status
, which is set up to handle these updates. ngrok or your deployed application URL should be used for the public portion of the URL.
Proper error handling involves using try-catch blocks in your webhook route to catch potential errors during validation, database interactions, or request processing. Return appropriate status codes (e.g., 403 for invalid signatures, 500 for server errors) so Twilio knows to retry or stop. Logging these errors is crucial for debugging.
Twilio doesn't guarantee status updates will arrive in order of occurrence. The guide suggests creating a new database log entry per update, preserving the received status and timestamp. This approach eliminates order dependency, and provides status update history via the database.
The Twilio Error Dictionary (available on Twilio's website) provides detailed explanations of error codes you might encounter during message sending or delivery. Refer to it when you receive ErrorCode
values in status callbacks to understand what went wrong and implement appropriate handling logic.
Use ngrok to expose your local development server, ensuring the NEXT_PUBLIC_APP_BASE_URL
in your .env file is set to your ngrok URL. Send a test SMS through your app's interface, or use a tool like Postman to send requests directly to the webhook, observing logs for request processing and status updates.
Storing the rawPayload
as JSON in your database captures all data sent by Twilio, including channel-specific details (like WhatsApp metadata) that might not be explicitly handled by your code. This provides valuable information for debugging, analysis, and adapting to future changes in Twilio's payload structure.
Idempotency means your webhook can handle receiving the same status update multiple times without adverse effects. Twilio might retry sending callbacks if there are network issues, so ensure your logic (database updates, etc.) functions correctly even if a callback is processed more than once. The example code provides idempotency for database entries via a unique constraint.
Log key events like webhook receipt, validation outcome, extracted data, and database operations. Include the MessageSid
in your logs to correlate events related to a specific message. Use structured logging and a centralized logging platform (e.g., Datadog, Logtail) for production-level logging and analysis.
Track Twilio SMS Delivery Status with Next.js Webhooks: Complete StatusCallback Guide
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.
Real-time status of sent messages – whether delivered, failed, or 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.
Build a Next.js application that sends SMS via Twilio and uses a dedicated API route to listen for and record status updates pushed by Twilio. Use Prisma for database interaction and implement security, error handling, and deployment best practices.
Citation1. From source: https://www.twilio.com/docs/messaging/guides/track-outbound-message-status, Title: Twilio StatusCallback Webhook Parameters 2024, Text: The properties included in Twilio's request to the StatusCallback URL vary by messaging channel and event type and are subject to change. Twilio occasionally adds new parameters without advance notice. When integrating with status callback requests, your implementation should be able to accept and correctly run signature validation on an evolving set of parameters. Twilio strongly recommends using the signature validation methods provided in the SDKs.
Project Overview and Goals
What You'll Build:
/api/twilio/status
) acting as a webhook endpoint for Twilio status callbacks.statusCallback
parameter.MessageSid
,MessageStatus
,ErrorCode
).Problem Solved:
Gain real-time visibility into the delivery lifecycle of outbound Twilio messages, enabling robust tracking, debugging, and follow-up actions based on message status (e.g., retrying failed messages, logging delivery confirmations).
Technologies Used:
System Architecture:
The system operates as follows:
StatusCallback
URL pointing back to the application.StatusCallback
URL (must be publicly accessible viangrok
during development or the deployed application URL in production)./api/twilio/status
).MessageSid
,MessageStatus
) in the database using Prisma.200 OK
HTTP response back to Twilio to acknowledge receipt.Prerequisites:
ngrok
) if testing locally.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
Initialize the Next.js project and install necessary dependencies.
Create a new Next.js App: Open your terminal and run:
Choose your preferred settings (TypeScript recommended). This guide assumes the
app
directory structure.Install Dependencies: Install the Twilio Node.js helper library and Prisma.
twilio
: Official library for interacting with the Twilio API.@prisma/client
: Prisma's database client.prisma
: Prisma's command-line tool for migrations and schema management.Initialize Prisma: Set up Prisma in your project. This creates a
prisma
directory with aschema.prisma
file and a.env
file for environment variables.--datasource-provider sqlite
: Uses SQLite for simplicity. Change this (e.g.,postgresql
,mysql
) for 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.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. Twilio uses this same token to sign webhook requests, serving as the 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 client-side access), it's used server-side in this guide to construct the fullstatusCallback
URL. Update this when using ngrok or deploying.Project Structure: Your basic structure will look something like this (using
app
router):Creating a Database Schema and Data Layer
Store incoming status updates in a database.
Define the Prisma Schema: Open
prisma/schema.prisma
and define a model to log message statuses.messageSid
(unique per message), thestatus
, an optionalerrorCode
, the fullrawPayload
as JSON for auditing, and atimestamp
.Apply the Schema to the Database: Run the Prisma command to create the database file (for SQLite) and the
MessageStatusLog
table.schema.prisma
definition. For production workflows, useprisma migrate dev
andprisma migrate deploy
.Create a Prisma Client Instance: Create a reusable Prisma client instance. Create
app/lib/prisma.ts
:Citation2. From source: https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices, Title: Prisma Client Singleton Pattern Next.js 2024, Text: The best practice is to instantiate a single instance of PrismaClient and save it on the globalThis object. This approach ensures only one instance of the client is created, preventing connection pool exhaustion during Next.js hot reloading in development mode. This pattern is specifically documented for Next.js 15 compatibility.
Implementing the Core Functionality (Webhook API Route)
Create the API route that receives
POST
requests from Twilio.Create the API Route File: Create the file
app/api/twilio/status/route.ts
.Implement the Webhook Handler:
Explanation:
TWILIO_AUTH_TOKEN
.req.text()
for signature validation.x-twilio-signature
header and the full request URL. Twilio uses the full URL in signature calculation.application/x-www-form-urlencoded
string into key-value pairs usingURLSearchParams
and convert to an object.twilio.validateRequest
with your Auth Token (acting as the secret), the signature, the full URL, and the parsed parameters object. This is the critical security step.403 Forbidden
.MessageSid
,MessageStatus
, andErrorCode
from the parsed parameters.P2002
) and log other DB errors.200 OK
response back to Twilio to acknowledge receipt.try...catch
logs unexpected errors and returns500 Internal Server Error
.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.Create the Sending API Route: Create
app/api/send-sms/route.ts
to handle the actual Twilio API call.Explanation:
POST
handler expecting JSON withto
andbody
.statusCallback
URL usingNEXT_PUBLIC_APP_BASE_URL
and the webhook route path (/api/twilio/status
).client.messages.create
, passing recipient, sender, body, and the crucialstatusCallback
URL.message.sid
and initialstatus
(e.g.,queued
).Local Development with
ngrok
: Twilio needs to sendPOST
requests to your application, so your local server (http://localhost:3000
) must be publicly accessible.ngrok
will display a ""Forwarding"" URL (e.g.,https://<random-subdomain>.ngrok-free.app
). This is your public URL..env
: Copy thehttps://...
ngrok URL and updateNEXT_PUBLIC_APP_BASE_URL
in your.env
file:Ctrl+C
) and restart (npm run dev
) your Next.js server to load the updated environment variable.Now, when sending a message:
statusCallback
URL sent to Twilio will use your publicngrok
URL.POST
requests to this URL.ngrok
forwards these requests tohttp://localhost:3000/api/twilio/status
.ngrok
terminal and your Next.js logs.Implementing Proper Error Handling, Logging, and Retry Mechanisms
/api/twilio/status
route usestry...catch
for validation, DB operations, and general processing.200
status codes (403
,500
) signals issues to Twilio. Twilio webhook retry behavior varies by configuration.P2002
for duplicates) prevents noise if Twilio resends data.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).MessageSid
for correlation.#rc=5
to your webhook URL. Without configuration, Twilio does not automatically retry on5xx
HTTP errors for SMS webhooks.200 OK
.Citation4. From source: https://www.twilio.com/docs/usage/webhooks/webhooks-connection-overrides and https://stackoverflow.com/questions/35449228/what-is-max-number-of-retry-attempts-for-twilio-sms-callback, Title: Twilio Webhook Retry Configuration 2024, Text: Twilio can retry webhooks up to 5 times on connection failures. By default, Twilio retries once on TCP connect or TLS handshake failures. You can configure additional retries (0-5 attempts) using connection overrides by adding #rc=5 to your webhook URL. For SMS/phone call webhooks, Twilio does not automatically retry to the same URL on 5xx errors without explicit configuration.
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.prisma.messageStatusLog.create
call (e.g., misspell a field) to observe the500
response and logs.MessageSid
. Verify the400 Bad Request
response.Adding Security Features
/api/twilio/status
usingtwilio.validateRequest
. Prevents fake status updates.ngrok
provides HTTPS locally. Deployment platforms (Vercel, Netlify) enforce HTTPS. HTTPS encrypts traffic using TLS and prevents replay attacks.TWILIO_ACCOUNT_SID
,TWILIO_AUTH_TOKEN
) using environment variables. Do not hardcode or commit them. Use platform secret management in production.MessageSid
,MessageStatus
) are implemented as good practice.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
sent
callback might arrive afterdelivered
.timestamp
field inMessageStatusLog
helps reconstruct history.Citation5. From source: https://www.twilio.com/docs/messaging/guides/track-outbound-message-status, Title: Twilio Webhook Order and Timing 2024, Text: Status callback requests are HTTP requests subject to differences in latency caused by changing network conditions. There is no guarantee that status callback requests always arrive at your endpoint in the order they were sent. Applications should be designed to handle out-of-order webhook delivery.
MessageStatus
isfailed
orundelivered
, check theErrorCode
field. Consult the Twilio Error Dictionary for meanings (e.g.,30003
– Unreachable,30007
– Carrier Violation). Log these codes.rawPayload
as JSON preserves this data.RawDlrDoneDate
: SMS/MMS callbacks might includeRawDlrDoneDate
(YYMMDDhhmm) for carrier's final status timestamp. Parse and store if needed.Implementing Performance Optimizations
@id
. The@unique
constraint onmessageSid
also creates an index, ensuring efficient lookups/checks.async/await
correctly, avoiding blocking operations.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.Adding Monitoring, Observability, and Analytics
/api/health
) returning200 OK
.MessageStatus
distribution (delivered vs. failed) and commonErrorCode
values.5xx
or403
errors from the webhook.ErrorCode
values.Troubleshooting and Caveats
ngrok
or deployment URL is correct and accessible.statusCallback
URL? Check the URL used inmessages.create
(includinghttps://
, domain, path/api/twilio/status
). Verify logs from the sending API route.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.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.paramsObject
in the example) are passed tovalidateRequest
. Do not pass the raw string or a re-serialized version.prisma/schema.prisma
matches the actual database structure (npx prisma db push
or migrations applied).DATABASE_URL
is correct and the database is reachable.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.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.Frequently Asked Questions About Twilio Delivery Status Callbacks
How do I set up Twilio StatusCallback webhooks in Next.js?
Set up Twilio StatusCallback webhooks in Next.js by creating an API route at
/api/twilio/status/route.ts
and passing the webhook URL in thestatusCallback
parameter when sending messages viaclient.messages.create()
. The webhook endpoint must be publicly accessible (use ngrok for local development) and validate incoming requests usingtwilio.validateRequest()
with your Auth Token to ensure requests originate from Twilio. Store status updates in a database using Prisma for tracking delivery, failures, and error codes.How does Twilio webhook signature validation work?
Twilio webhook signature validation uses HMAC-SHA1 to sign requests with your Auth Token as the secret key. Extract the
x-twilio-signature
header from incoming requests and validate it usingtwilio.validateRequest(authToken, signature, fullUrl, paramsObject)
from the Twilio Node.js SDK. This prevents fake status updates from unauthorized sources. Always use the exact full URL Twilio called (including protocol and query parameters) and the parsed request body parameters for validation. Twilio strongly recommends using SDK validation methods rather than implementing custom validation.What information does Twilio send in StatusCallback webhooks?
Twilio sends
MessageSid
(unique message identifier),MessageStatus
(queued, sent, delivered, failed, undelivered),ErrorCode
(if delivery failed), and channel-specific parameters in StatusCallback webhooks. The parameters vary by messaging channel (SMS, MMS, WhatsApp) and are subject to change without notice. Store the completerawPayload
as JSON to preserve all webhook data for debugging and future use. Common status values include 'queued' (accepted), 'sent' (dispatched to carrier), 'delivered' (confirmed by carrier), 'failed' (permanent failure), and 'undelivered' (temporary failure).How do I handle failed SMS deliveries with Twilio webhooks?
Handle failed SMS deliveries by checking the
MessageStatus
field for 'failed' or 'undelivered' values in your webhook handler. Extract theErrorCode
field to determine the failure reason – consult the Twilio Error Dictionary for meanings (e.g., 30003 for unreachable numbers, 30007 for carrier violations). Log error codes to your database for analysis and implement retry logic for transient failures. For permanent failures (invalid numbers, blocked content), mark messages as undeliverable and notify users through alternative channels. Design your webhook endpoint to be idempotent to handle duplicate status updates.Does Twilio automatically retry failed webhook deliveries?
Twilio does not automatically retry webhook deliveries on 5xx HTTP errors for SMS callbacks by default. By default, Twilio retries once on TCP connect or TLS handshake failures only. Configure additional retry attempts (up to 5 times) by appending connection override parameters like
#rc=5
to your webhook URL. Ensure your endpoint responds with200 OK
within the timeout period and implements idempotent processing to handle duplicate status updates gracefully. Return500
status codes for temporary errors where retry is appropriate, and200 OK
for successfully processed requests (including duplicates).What database schema should I use for storing Twilio status updates?
Use a database schema with
messageSid
(unique identifier),status
(current message state),errorCode
(optional error details),rawPayload
(full JSON webhook data), andtimestamp
(when status was received) fields. SetmessageSid
as unique to prevent duplicate entries. Store the complete webhook payload as JSON for debugging and accessing channel-specific fields. Create indexes onmessageSid
for efficient lookups and consider addingcreatedAt
timestamps for time-series analysis. Use Prisma with the singleton pattern to prevent connection pool exhaustion during Next.js hot reloading in development.How do I secure Twilio webhooks in production?
Secure Twilio webhooks by implementing signature validation using
twilio.validateRequest()
, hosting endpoints on HTTPS (prevents replay attacks via TLS encryption), storing credentials in environment variables (never hardcode), and optionally adding rate limiting for high-traffic scenarios. Always validate thex-twilio-signature
header before processing webhook data. Use platform secret management (Vercel Environment Variables, AWS Secrets Manager) for production deployments. Consider adding IP allowlisting for Twilio's webhook source IPs and implement proper error handling to avoid exposing internal system details in error responses.How do I test Twilio webhooks locally with ngrok?
Test Twilio webhooks locally by installing ngrok (
ngrok.com/download
), runningngrok http 3000
in a separate terminal to expose your local server, copying the HTTPS forwarding URL (e.g.,https://abc123.ngrok-free.app
), updatingNEXT_PUBLIC_APP_BASE_URL
in your.env
file with the ngrok URL, and restarting your Next.js development server. The webhook endpoint will be accessible athttps://abc123.ngrok-free.app/api/twilio/status
. Monitor incoming requests in the ngrok terminal and your Next.js logs. Free ngrok sessions expire and generate new URLs on each restart, requiring environment variable updates.Why are Twilio status callbacks arriving out of order?
Twilio status callbacks arrive out of order because they are HTTP requests subject to network latency variations. Twilio does not guarantee callbacks arrive in chronological order – a 'sent' callback might arrive after 'delivered'. Design your application to handle out-of-order delivery by creating a new log entry for each status update with timestamps rather than updating a single record. Use the
timestamp
field to reconstruct the actual event timeline. Consider implementing state machine logic if you need to enforce status progression rules, but accept that callbacks reflect network delivery timing rather than actual event sequence.What Twilio error codes should I monitor in webhook callbacks?
Monitor critical Twilio error codes including 30003 (unreachable destination), 30004 (message blocked), 30005 (unknown destination), 30006 (landline or unreachable carrier), 30007 (carrier violation/spam filter), 30008 (unknown error), and 21610 (message not sent due to account suspension). Log error codes to your database with full context for analysis. Set up alerts for high error rates or specific codes indicating systemic issues. Consult the Twilio Error Dictionary for complete error code meanings and recommended remediation steps. Implement different handling strategies for temporary errors (retry) versus permanent failures (mark undeliverable).