code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

How to Track Twilio SMS Delivery Status in RedwoodJS: Webhooks & Callbacks Guide

Step-by-step guide to implement Twilio SMS delivery status callbacks in RedwoodJS. Set up webhooks, validate signatures, and track message delivery with Prisma integration.

Tracking SMS delivery status is essential for reliable messaging applications. Twilio status callbacks (webhooks) notify your application when message status changes—from queued to sent, delivered, or failed. This allows you to monitor delivery in real-time, handle failures, and provide users with accurate delivery updates.

This guide shows you how to implement Twilio SMS delivery status webhooks in your RedwoodJS application. You'll learn to set up a webhook endpoint, validate Twilio callback requests securely, and track message status using Prisma database integration.

Project Goals:

  • Send SMS messages via the Twilio API from your RedwoodJS application.
  • Configure Twilio to send status updates to a dedicated webhook endpoint in your RedwoodJS app.
  • Securely handle incoming webhook requests from Twilio.
  • Store message details and update their delivery status in your database using Prisma.
  • Build a robust foundation for reliable SMS status tracking.

Technologies Used:

  • RedwoodJS: A full-stack JavaScript/TypeScript framework for the web. You'll use its structure and tooling for both frontend (React) and backend (Node.js, GraphQL API, Prisma) development.
  • Node.js: The runtime environment for your RedwoodJS API side.
  • Twilio Programmable Messaging: The Twilio service you'll use for sending SMS and receiving status callbacks.
  • Prisma: The database toolkit RedwoodJS uses for schema definition, migrations, and database access.
  • PostgreSQL (or SQLite/MySQL): Your underlying database where you'll store message statuses.

System Architecture:

text
+-----------------+      +-----------------+      +-----------------+      +------------------+
| RedwoodJS Web   |----->| RedwoodJS API   |----->|   Twilio API    |----->| Carrier Network  |
| (React)         |      | (Node.js/GraphQL)|      | (Send SMS)      |      | (Delivers SMS)   |
+-----------------+      +-----------------+      +-----------------+      +------------------+
        ^                     |        ^                                         |
        | User Action         |        | Send Request                            | SMS Status Update
        |                     |        | w/ StatusCallback URL                   v
        |                     |        +-----------------------------------------+
        |                     |                                                  |
        |                     +-----------------------+                          |
        |                     | Update DB             |                          |
        |                     v                       |                          |
        |               +-----------------+ <---------+   +-----------------+    |
        |               | Database        |             |   Twilio Webhook| <--+
        |               | (Prisma)        |             |  (Status Update)|
        +---------------+                 +-------------+-----------------+
  1. A user action (or backend process) triggers your RedwoodJS API to send an SMS.
  2. Your RedwoodJS API calls the Twilio API, including a statusCallback URL pointing back to a specific endpoint on your RedwoodJS API.
  3. Twilio sends the SMS message via the carrier network.
  4. As the message status changes (e.g., sent, delivered, failed), Twilio sends an HTTP POST request (webhook) to your specified statusCallback URL.
  5. Your RedwoodJS API endpoint receives the webhook, validates its authenticity, parses the status, and updates the corresponding message record in your database.

Prerequisites:

  • Node.js: Version 18.x or higher required for RedwoodJS v7; Node.js 20.17.0+ required for RedwoodJS v8 (released June 2024). Node.js 21+ may cause compatibility issues with some deploy targets like AWS Lambda.
  • RedwoodJS: This guide is compatible with RedwoodJS v6+. For v8+, note that Webpack support has been dropped in favor of Vite.
  • Yarn: v1.15+ for RedwoodJS v7; v4.1.1+ required for RedwoodJS v8.
  • A Twilio account with Account SID, Auth Token, and a Twilio phone number capable of sending SMS. Find these in your Twilio Console.
  • Basic understanding of RedwoodJS concepts (CLI, services, functions).
  • A tool like ngrok for testing webhooks locally during development.

1. Setting Up Your RedwoodJS Project for SMS Tracking

Create a new RedwoodJS project if you don't have one:

bash
yarn create redwood-app ./twilio-status-app
cd twilio-status-app

Install Twilio Helper Library:

Navigate to the API side and install the official Twilio Node.js helper library:

bash
cd api
yarn add twilio
cd .. # Return to project root

Configure Environment Variables:

RedwoodJS uses .env files for environment variables. Create a .env file in your project root if it doesn't exist:

plaintext
# .env

# Twilio Credentials
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token
TWILIO_PHONE_NUMBER=+15017122661 # Your Twilio phone number

# Base URL for API callbacks (used for ngrok/deployment)
# Example for ngrok: https://<your-ngrok-subdomain>.ngrok.io
# Example for deployment: https://your-app-domain.com
API_BASE_URL=http://localhost:8911 # Default local dev, update for testing/deployment
  • TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN: Find these on your Twilio Console dashboard. Keep these secret! Never commit them to version control.
  • TWILIO_PHONE_NUMBER: Your Twilio number for sending messages.
  • API_BASE_URL: The publicly accessible base URL where your RedwoodJS API runs. Use your ngrok forwarding URL during local development. In production, use your deployed application's domain. You need this to construct the statusCallback URL.

Project Structure:

RedwoodJS organizes code into web (frontend) and api (backend) sides. You'll primarily work within the api directory:

  • api/src/functions/: For serverless function handlers (like your webhook endpoint).
  • api/src/services/: For business logic and interacting with third-party APIs (like Twilio).
  • api/db/schema.prisma: For database schema definition.
  • api/src/lib/: For shared utilities (like your Twilio client instance).

2. Implementing SMS Delivery Status Tracking: Core Functionality

You need two main pieces: logic to send an SMS and specify the callback URL, and logic to receive and process the callback.

2.1. Create Database Schema to Store Message Status

Define a model in your Prisma schema to store message information, including its status.

prisma
// api/db/schema.prisma

datasource db {
  provider = "postgresql" // Or "sqlite", "mysql"
  url      = env("DATABASE_URL")
}

generator client {
  provider      = "prisma-client-js"
  binaryTargets = "native"
}

// Add this model
model Message {
  id           String   @id @default(cuid()) // Unique database ID
  sid          String   @unique // Twilio Message SID
  to           String   // Recipient phone number
  from         String   // Sender phone number (Twilio number)
  body         String?  // Message content
  status       String   // e.g., queued, sending, sent, delivered, failed, undelivered
  errorCode    Int?     // Twilio error code if failed/undelivered
  errorMessage String?  // Twilio error message if failed/undelivered
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
}

Apply Migrations:

Generate and apply the database migration:

bash
yarn rw prisma migrate dev --name add_message_model

This creates the Message table in your database.

Understanding Twilio Message Status Values:

Twilio status callbacks include the MessageStatus parameter indicating the current state of your message. The possible values are:

  • queued: Message is queued and waiting to be sent
  • sending: Message is currently being sent to the carrier
  • sent: Message has been sent from Twilio to the carrier
  • delivered: Carrier confirmed delivery to the recipient's device
  • undelivered: Carrier could not deliver the message to the device
  • failed: Message failed to send (API request failed or carrier rejected)

Note: The ErrorCode field only appears in callback events for failed or undelivered messages. Common error codes include:

  • 30003: Unreachable destination handset (device powered off or out of service)
  • 30005: Unknown destination handset (number no longer in service)
  • 30008: Unknown error (generic carrier error with no details)

Billing Note: Starting September 30, 2024, Twilio charges a "Failed message processing fee" for most messages in failed status. See Twilio's Error and Warning Dictionary for a complete list of error codes.

2.2. Creating a Twilio Client Instance

Initialize the Twilio client once and reuse it throughout your application.

javascript
// api/src/lib/twilio.js

import twilio from 'twilio'

const accountSid = process.env.TWILIO_ACCOUNT_SID
const authToken = process.env.TWILIO_AUTH_TOKEN

if (!accountSid || !authToken) {
  throw new Error(
    'Twilio credentials (TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) are required. Check your .env file.'
  )
}

export const twilioClient = twilio(accountSid, authToken)

export const twilioPhoneNumber = process.env.TWILIO_PHONE_NUMBER

if (!twilioPhoneNumber) {
  throw new Error(
    'Twilio phone number (TWILIO_PHONE_NUMBER) is required. Check your .env file.'
  )
}

2.3. Service for Sending SMS

Create a RedwoodJS service to encapsulate the logic for sending SMS messages via Twilio and creating the initial record in your database.

bash
yarn rw g service sms

Implement the sendSms function in the generated service file.

javascript
// api/src/services/sms/sms.js

import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
import { twilioClient, twilioPhoneNumber } from 'src/lib/twilio'

// Construct the full callback URL using the base URL from environment variables
// Ensure API_BASE_URL ends without a trailing slash
const apiBaseUrl = (process.env.API_BASE_URL || '').replace(/\/$/, '')
const statusCallbackUrl = `${apiBaseUrl}/.redwood/functions/twilioCallback`

export const sendSms = async ({ to, body }) => {
  if (!to || !body) {
    throw new Error("Both 'to' phone number and 'body' are required.")
  }
  if (!apiBaseUrl) {
     logger.error('API_BASE_URL environment variable is not set. Cannot construct StatusCallback URL.')
     throw new Error('Application configuration error: API_BASE_URL is missing.')
  }

  logger.info(`Attempting to send SMS to ${to}`)

  try {
    // Send message via Twilio, providing the StatusCallback URL
    const twilioMessage = await twilioClient.messages.create({
      body: body,
      from: twilioPhoneNumber,
      to: to,
      statusCallback: statusCallbackUrl, // Tell Twilio where to send updates
    })

    logger.info(
      `SMS queued successfully with SID: ${twilioMessage.sid}, Status: ${twilioMessage.status}`
    )

    // Create initial record in the database
    const storedMessage = await db.message.create({
      data: {
        sid: twilioMessage.sid,
        to: twilioMessage.to,
        from: twilioMessage.from,
        body: twilioMessage.body,
        status: twilioMessage.status, // Initial status from Twilio (e.g., 'queued')
      },
    })

    logger.info(
      `Stored message in DB with ID: ${storedMessage.id} and SID: ${storedMessage.sid}`
    )

    // Return the database record, not the raw Twilio response
    return storedMessage
  } catch (error) {
    logger.error(`Error sending SMS to ${to}: ${error.message}`, error)
    throw new Error(`Failed to send SMS: ${error.message}`)
  }
}

// Service function to retrieve message status from DB
export const getMessageStatus = async ({ sid }) => {
  logger.info(`Retrieving status for message SID: ${sid}`)
  const message = await db.message.findUnique({
    where: { sid },
  })

  if (!message) {
    logger.warn(`Message with SID ${sid} not found in database.`)
    return null
  }

  return message
}

export const messages = () => {
  return db.message.findMany()
}

Explanation:

  1. Imports: Import the Prisma client (db), Redwood's logger, and your configured twilioClient and twilioPhoneNumber.
  2. statusCallbackUrl: Construct the full URL for your webhook endpoint. It combines the API_BASE_URL (which must be publicly accessible) with the conventional Redwood path for functions (/.redwood/functions/) and your specific function name (twilioCallback). Set API_BASE_URL correctly for your environment.
  3. twilioClient.messages.create: Call the Twilio API to send the message.
    • body, from, to: Standard message parameters.
    • statusCallback: Tell Twilio to send POST requests with status updates for this specific message to your twilioCallback function URL.
  4. Database Creation: After successfully queueing the message with Twilio, create a record in your Message table using db.message.create. Store the sid returned by Twilio, along with other details and the initial status (usually queued).
  5. Error Handling: The try...catch block logs errors. Production applications need more robust error handling.
  6. Return Value: Return the message record created in your database.

2.4. Handle Twilio Status Callback Webhooks

Create the RedwoodJS function that Twilio will call.

bash
yarn rw g function twilioCallback --typescript=false # Or --typescript=true if using TS

Implement the handler logic in the generated file. This endpoint needs to:

  1. Receive the POST request from Twilio.
  2. Validate the request signature to ensure it genuinely came from Twilio.
  3. Parse the MessageSid and MessageStatus (and ErrorCode if present) from the request body.
  4. Update the corresponding message record in the database.
  5. Return a 200 OK response to Twilio.
javascript
// api/src/functions/twilioCallback.js

import { logger } from 'src/lib/logger'
import { db } from 'src/lib/db'
import { validateRequest } from 'twilio'

/**
 * @param {import("@redwoodjs/api").APIGatewayEvent} event – The incoming request event
 * @param {import("@redwoodjs/api").Context} context – The context object
 */
export const handler = async (event, context) => {
  logger.info('Received Twilio status callback request')

  // --- 1. Validate Twilio Request Signature ---
  const twilioSignature = event.headers['x-twilio-signature']
  const authToken = process.env.TWILIO_AUTH_TOKEN
  // Construct the full URL the request was made to.
  // Ensure API_BASE_URL ends without a trailing slash
  const apiBaseUrl = (process.env.API_BASE_URL || '').replace(/\/$/, '')
  const callbackUrl = `${apiBaseUrl}${event.path}` // event.path includes /.redwood/functions/twilioCallback

  if (!authToken || !apiBaseUrl) {
     logger.error('Missing TWILIO_AUTH_TOKEN or API_BASE_URL environment variable. Cannot validate request.')
     return {
      statusCode: 500,
      body: 'Server configuration error.',
     }
  }

  // Parse the body – RedwoodJS v6+ automatically parses common content types
  // including 'application/x-www-form-urlencoded' into event.body (as an object).
  const params = event.body

  // validateRequest expects the raw POST body params as an object.
  const isValid = validateRequest(authToken, twilioSignature, callbackUrl, params || {})

  if (!isValid) {
    logger.error('Invalid Twilio signature. Request rejected.')
    return {
      statusCode: 403, // Forbidden
      body: 'Invalid Twilio signature.',
    }
  }

  logger.info('Twilio signature validated successfully.')

  // --- 2. Parse Request Body ---
  const messageSid = params?.MessageSid
  const messageStatus = params?.MessageStatus
  const errorCode = params?.ErrorCode ? parseInt(params.ErrorCode, 10) : null
  const errorMessage = params?.ErrorMessage || null

  if (!messageSid || !messageStatus) {
    logger.warn('Missing MessageSid or MessageStatus in callback body.')
    // Even if parameters are missing, signature was valid, so return 200 to avoid retries.
    return {
      statusCode: 200,
      headers: { 'Content-Type': 'text/xml' },
      body: '<Response></Response>',
    }
  }

  logger.info(
    `Processing status update for SID: ${messageSid}, New Status: ${messageStatus}, ErrorCode: ${errorCode}`
  )

  // --- 3. Update Database ---
  try {
    const updatedMessage = await db.message.update({
      where: { sid: messageSid },
      data: {
        status: messageStatus,
        errorCode: errorCode,
        errorMessage: errorMessage,
      },
    })

    logger.info(
      `Successfully updated status for message SID: ${messageSid} to ${messageStatus} in DB (ID: ${updatedMessage.id})`
    )
  } catch (error) {
    // Handle cases where the message SID might not be found
    if (error.code === 'P2025') { // Prisma code for Record not found
      logger.error(
        `Message with SID ${messageSid} not found in database for update.`
      )
    } else {
      logger.error(
        `Database error updating status for SID ${messageSid}: ${error.message}`,
        error
      )
      return {
        statusCode: 200, // Acknowledge receipt despite DB error to stop retries
        headers: { 'Content-Type': 'text/xml' },
        body: '<Response></Response>',
      }
    }
  }

  // --- 4. Respond to Twilio ---
  // Twilio expects a 200 OK response to acknowledge receipt.
  return {
    statusCode: 200,
    headers: { 'Content-Type': 'text/xml' },
    body: '<Response></Response>',
  }
}

Explanation:

  1. Signature Validation: This is critical for security. validateRequest from the twilio library checks if the x-twilio-signature header matches a signature calculated using your TWILIO_AUTH_TOKEN, the request URL (callbackUrl), and the POST parameters (params). This verifies the request originated from Twilio and wasn't tampered with. Reject requests with invalid signatures using a 403 Forbidden status.
  2. Parsing: Extract MessageSid, MessageStatus, ErrorCode, and ErrorMessage from the request parameters (event.body). Note: This code assumes you are using RedwoodJS v6 or later, which automatically parses application/x-www-form-urlencoded request bodies into the event.body object.
  3. Database Update: Use db.message.update to find the message record by its unique sid and update the status, errorCode, and errorMessage fields. Include error handling, especially for the case where the message might not (yet) exist in the DB (Prisma error P2025).
  4. Response: Return a 200 OK status code with an empty TwiML <Response></Response> body. This acknowledges receipt to Twilio, preventing unnecessary retries. Even if a database error occurs after validating the signature, returning 200 is often preferred to avoid excessive retries for potentially transient DB issues.

3. Exposing the Send Functionality (API Layer)

While the callback handler is a direct webhook, you need a way to trigger the sendSms service. A common RedwoodJS approach is via a GraphQL mutation.

Define GraphQL Schema:

graphql
// api/src/graphql/sms.sdl.ts

export const schema = gql`
  type Message {
    id: String!
    sid: String!
    to: String!
    from: String!
    body: String
    status: String!
    errorCode: Int
    errorMessage: String
    createdAt: DateTime!
    updatedAt: DateTime!
  }

  type Mutation {
    sendSms(to: String!, body: String!): Message! @requireAuth
    # Or use @skipAuth if authentication isn't needed for this action
  }

  # Query to fetch status by SID
  type Query {
    messageStatus(sid: String!): Message @requireAuth
  }
`

Implement Resolvers:

Redwood maps the SDL definitions to the service functions. Ensure your api/src/services/sms/sms.js file exports functions matching the mutation and query names (sendSms and messageStatus). We already did this in Step 2.3.

Call this mutation from your RedwoodJS web side (or any GraphQL client) after implementing authentication (@requireAuth).

Example Frontend Call (React Component):

javascript
// web/src/components/SendMessageForm/SendMessageForm.js
import { useMutation, gql } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import {
  Form,
  TextField,
  TextAreaField,
  Submit,
  FieldError,
  Label,
} from '@redwoodjs/forms'

const SEND_SMS_MUTATION = gql`
  mutation SendSmsMutation($to: String!, $body: String!) {
    sendSms(to: $to, body: $body) {
      id
      sid
      status
      to
    }
  }
`

const SendMessageForm = () => {
  const [sendSms, { loading, error }] = useMutation(SEND_SMS_MUTATION, {
    onCompleted: (data) => {
      toast.success(
        `SMS to ${data.sendSms.to} queued! SID: ${data.sendSms.sid}, Status: ${data.sendSms.status}`
      )
    },
    onError: (error) => {
      toast.error(`Error sending SMS: ${error.message}`)
    },
  })

  const onSubmit = (data) => {
    sendSms({ variables: data })
  }

  return (
    <Form onSubmit={onSubmit}>
      {error && <p style={{ color: 'red' }}>Error: {error.message}</p>}

      <Label name="to" htmlFor="to" errorClassName="error">To Phone Number:</Label>
      <TextField
        name="to"
        id="to"
        validation={{ required: true, pattern: /^\+?[1-9]\d{1,14}$/ }}
        errorClassName="error"
      />
      <FieldError name="to" className="error" />

      <Label name="body" htmlFor="body" errorClassName="error">Message:</Label>
      <TextAreaField
        name="body"
        id="body"
        validation={{ required: true }}
        errorClassName="error"
      />
      <FieldError name="body" className="error" />

      <Submit disabled={loading}>{loading ? 'Sending…' : 'Send SMS'}</Submit>
    </Form>
  )
}

export default SendMessageForm

4. Configure Twilio Status Callback URL

  • API Credentials: As covered in Step 1, store TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN securely in your .env file and ensure they are available as environment variables in your deployment environment. Access them via process.env.
  • Twilio Phone Number: Store your TWILIO_PHONE_NUMBER in .env.
  • Status Callback URL:
    • Per-Message (Implemented Above): By providing the statusCallback parameter in the client.messages.create call, you tell Twilio where to send updates for that specific message. This is flexible but requires constructing the correct URL in your code. Ensure API_BASE_URL is correctly set.
    • Messaging Service Level: Alternatively, configure a default Status Callback URL within a Twilio Messaging Service. If you send messages using a messagingServiceSid instead of a from number, and don't provide a statusCallback parameter in the API call, Twilio will use the URL configured on the service.
      • Go to Twilio Console > Messaging > Services.
      • Select or create a Messaging Service.
      • Under "Integration", configure the "Status callback URL". Enter the full public URL to your twilioCallback function (e.g., https://your-app-domain.com/.redwood/functions/twilioCallback).
      • When sending, use messagingServiceSid instead of from.
      • Note: A statusCallback URL provided in the API call overrides the Messaging Service setting for that specific message.

5. Handle Errors and Retry Logic for Delivery Callbacks

  • Error Handling:
    • Wrap Twilio API calls (messages.create) in try...catch blocks in your service (sendSms). Log errors clearly.
    • In the callback handler (twilioCallback), handle potential database errors gracefully (e.g., message not found P2025, other DB connection issues). Return 200 OK even on handled DB errors post-signature validation to prevent excessive Twilio retries.
    • Validate incoming callback parameters (MessageSid, MessageStatus).
  • Logging:
    • Use Redwood's built-in logger (import { logger } from 'src/lib/logger').
    • Log key events: sending attempt, successful queuing, received callback, signature validation result, database update attempt/success/failure. Include MessageSid in logs for correlation.
    • Configure appropriate log levels for development vs. production.
  • Twilio Retries:
    • If your twilioCallback endpoint returns a non-2xx status code (e.g., 4xx, 5xx) or times out, Twilio will retry the request.
    • Retries occur with exponential backoff for a certain period.
    • Ensure your endpoint is idempotent: processing the same callback multiple times should not cause incorrect state changes (e.g., use update with where: { sid: ... }, which is generally safe).
    • Validate signatures and return 200 OK promptly (even on some internal errors after validation) to prevent unnecessary retries.

6. Database Schema and Data Layer

  • The Message model in schema.prisma (Step 2.1) defines the structure.
  • Prisma migrations (yarn rw prisma migrate dev) handle schema changes (Step 2.1).
  • The data access layer is implemented within the RedwoodJS service (sms.js) using the Prisma client (db) for creating and updating records (Steps 2.3 & 2.4).
  • Performance: For high volume, ensure the sid column in the Message table has an index (Prisma adds @unique which typically creates an index). Database connection pooling (handled by Prisma) is essential.

7. Secure Your Webhook Endpoint with Signature Validation

  • Webhook Signature Validation: This is the most critical security measure (implemented in Step 2.4 using validateRequest). It prevents unauthorized actors from forging status updates. Never skip this step.
    • Twilio cryptographically signs all HTTP requests using HMAC-SHA1 with your Auth Token as the key, providing the hash in the X-Twilio-Signature header.
    • Always use Twilio's SDK helper libraries for validation rather than implementing your own. The helper libraries handle signature validation correctly and account for evolving parameters.
    • Twilio occasionally adds new parameters to webhooks without advance notice, so your implementation must accept and validate an evolving parameter set.
  • HTTPS Endpoints (Required): Host your webhook endpoints on https:// URLs with valid SSL certificates. While signature validation prevents request tampering, HTTPS prevents replay attacks where captured requests could be resent by attackers.
    • Enable SSL Certificate Validation in your Twilio Console project Settings to enforce validation on all webhooks.
    • Avoid certificate pinning – Twilio strongly recommends against this outdated practice as certificates can be rotated at any time.
  • Environment Variables: Protect your TWILIO_AUTH_TOKEN and TWILIO_ACCOUNT_SID. Do not commit them to your repository. Use .env locally and secure environment variable management in your deployment environment.
  • Input Validation: Validate inputs to the sendSms mutation (e.g., format of the to number, length of the body). Redwood's forms provide basic validation helpers.
  • Authentication/Authorization: Use Redwood's built-in auth (@requireAuth) to protect the GraphQL mutation (sendSms) so only authenticated users can trigger sending messages, if applicable. The callback endpoint itself is secured by signature validation, not user login.
  • Rate Limiting: Consider adding rate limiting to the sendSms mutation endpoint to prevent abuse. The callback endpoint implicitly relies on Twilio's rate limiting for sending webhooks.
  • Additional Security Layers: Add HTTP Basic Authentication to webhook URLs for defense in depth, requiring username/password combinations to access endpoints.

8. Handling Special Cases

  • Callback Order: Twilio status callbacks might arrive out of order due to network latency. A message might transition queuedsendingsent quickly, but the callbacks could arrive as queuedsentsending. Design your logic to handle this. Usually, updating with the latest received status is sufficient, but you might store timestamps if order is critical. The RawDlrDoneDate property (present for delivered/undelivered SMS/MMS) can provide a carrier timestamp.
  • Evolving Webhook Parameters: Twilio may add new parameters to status callback requests without advance notice. The properties included vary by messaging channel and event type. Design your webhook handler to gracefully accept additional parameters beyond the core fields (MessageSid, MessageStatus, ErrorCode). Both SmsStatus and MessageStatus fields are included in callbacks for backward compatibility.
  • Message Not Found in DB: The callback might arrive before the sendSms function finishes writing the initial record to the database (a race condition, less likely but possible). The error handling in twilioCallback (checking for Prisma P2025) should log this. You might implement a small delay/retry mechanism within the callback if this becomes a frequent issue, but often logging and returning 200 OK is sufficient.

9. Performance Optimizations

  • Webhook Handler Speed: Keep the twilioCallback function lightweight. Perform the essential tasks (validate signature, parse body, update DB) and return 200 OK quickly. Defer any heavy processing (e.g., complex analytics, triggering other workflows) to a background job queue if necessary.
  • Database Indexing: Ensure the sid column in the Message table is indexed for fast lookups during updates (@unique usually ensures this).
  • Asynchronous Operations: Use async/await correctly to avoid blocking the Node.js event loop.

10. Monitoring, Observability, and Analytics

  • Logging: Centralized logging (using platforms like Logtail, Datadog, Sentry) is crucial for monitoring activity and diagnosing issues in production. Ensure logs include the MessageSid.
  • Error Tracking: Integrate an error tracking service (like Sentry) to capture and alert on exceptions in both the sendSms service and the twilioCallback function. RedwoodJS has integrations available.
  • Health Checks: Implement a basic health check endpoint for your API side (e.g., /healthz) that verifies database connectivity. Monitor this endpoint.
  • Twilio Console: Utilize the Twilio Console's Messaging Logs and Debugger tools to inspect message statuses, delivery errors, and webhook request details.
  • Key Metrics: Monitor:
    • Rate of outgoing SMS messages.
    • Rate of incoming status callbacks.
    • Latency of the twilioCallback handler.
    • Rate of signature validation failures (could indicate misconfiguration or attack).
    • Database update success/failure rates for callbacks.
    • Distribution of message statuses (delivered, failed, undelivered).

11. Troubleshooting Common Status Callback Issues

  • Callbacks Not Received:
    • Incorrect API_BASE_URL / statusCallback URL: Double-check the URL being sent to Twilio and ensure it's publicly accessible. Use ngrok for local testing. Verify API_BASE_URL is set in your environment.
    • Firewall Issues: Ensure your server/deployment environment allows incoming POST requests from Twilio's IP ranges (though signature validation is generally preferred over IP whitelisting).
    • Function Errors: Check your API logs (yarn rw log api or your production logging service) for errors in the twilioCallback handler preventing it from returning 200 OK.
    • Missing statusCallback Parameter: Ensure you are actually providing the statusCallback URL when calling messages.create or that it's correctly configured on the Messaging Service.
  • Signature Validation Fails:
    • Incorrect TWILIO_AUTH_TOKEN: Verify the token in your .env matches the one in the Twilio console.
    • Incorrect URL in validateRequest: Ensure the callbackUrl passed to validateRequest exactly matches the URL Twilio is sending the request to, including protocol (http/https), domain, and path (/.redwood/functions/twilioCallback). Check for trailing slashes or port number discrepancies, especially behind proxies or load balancers.
    • Body Parsing Issues: Ensure the params object passed to validateRequest accurately represents the raw POST body parameters sent by Twilio (usually application/x-www-form-urlencoded). If event.body isn't automatically parsed correctly in your environment, manual parsing is required.
  • Database Errors:
    • Connection Issues: Verify database credentials and network connectivity between your API function and the database.
    • Record Not Found (P2025): As discussed, this can happen due to race conditions. Log the error and return 200 OK if the signature was valid.
    • Schema Mismatches: Ensure your database schema is up-to-date with your schema.prisma file (yarn rw prisma migrate dev).
  • ngrok Specifics:
    • When using ngrok, the public URL changes each time you restart it. Remember to update your API_BASE_URL in .env (or wherever it's set) and potentially restart your Redwood dev server (yarn rw dev) to reflect the new URL used in the statusCallback.
    • Use the https URL provided by ngrok.

Learn more about SMS delivery tracking and RedwoodJS integration:


Frequently Asked Questions

How do I check SMS delivery status in Twilio?

Set up a status callback webhook endpoint in your application and provide the webhook URL when sending messages via the Twilio API. Twilio will POST status updates to your endpoint as the message progresses through queued, sent, delivered, or failed states.

What is the difference between Twilio sent and delivered status?

"Sent" means Twilio successfully handed the message to the carrier network. "Delivered" means the carrier confirmed the message reached the recipient's device. Delivery confirmation depends on carrier support and may not be available for all networks.

Why are my Twilio SMS messages undelivered?

Common causes include invalid phone numbers, devices powered off or out of service (error 30003), numbers no longer in service (error 30005), carrier filtering, or network issues. Check the ErrorCode in your webhook callback for specific failure reasons.

How much does Twilio charge for failed SMS messages?

As of September 30, 2024, Twilio charges a $0.001 failed message processing fee per message that terminates in "Failed" status. This applies to most failure scenarios but not all error codes.

Frequently Asked Questions

how to track sms delivery status with twilio

Twilio's status callbacks (webhooks) notify your application about message status changes. You need to configure a webhook endpoint in your application to receive these real-time updates, which include delivery, failure, or in-transit statuses. This enables better communication and debugging.

what is a twilio status callback

A Twilio status callback is a webhook that Twilio sends to your application to provide updates on the delivery status of your SMS messages. These updates are sent as HTTP POST requests to a URL you specify, containing details like message SID and status (e.g., queued, sent, delivered, failed).

why use twilio for sms delivery

Twilio offers a reliable and programmable messaging API that simplifies sending SMS and receiving delivery status updates via webhooks. It handles the complexities of carrier networks, providing a robust foundation for SMS communication.

when to use twilio status callbacks

Use Twilio status callbacks whenever real-time tracking of SMS message delivery is important. This includes scenarios requiring user notifications, message debugging, delivery confirmation, and analytics on message success/failure rates.

can I use twilio with redwoodjs

Yes, this article provides a comprehensive guide on integrating Twilio's SMS delivery status callbacks into a RedwoodJS application. You'll set up a webhook endpoint, validate requests securely, and update message statuses in your database.

how to set up twilio status callbacks in redwoodjs

Set up a serverless function in your RedwoodJS API to act as the webhook endpoint. Then, configure your Twilio account to send status updates to this endpoint by providing its URL when sending messages or configuring it at the Messaging Service level in the Twilio Console.

what is the statuscallback url in twilio

The `statusCallback` URL is the endpoint in your application that Twilio will send HTTP POST requests to with message delivery status updates. It must be a publicly accessible URL and is provided when sending the message or configured in your Twilio Messaging Service.

why validate twilio webhook signatures

Validating Twilio webhook signatures is crucial for security. It ensures that incoming requests genuinely originated from Twilio and haven't been tampered with, protecting your application from fraudulent status updates.

how to secure twilio webhooks in redwoodjs

Use the `validateRequest` function from the Twilio Node.js helper library to verify the signature of incoming webhook requests. This function compares the signature in the `x-twilio-signature` header with a calculated signature using your auth token and request details, ensuring authenticity.

what database is used for twilio message status

The article recommends Prisma as the database toolkit with RedwoodJS, allowing for flexible schema definition, migrations, and data access. It supports PostgreSQL, SQLite, and MySQL as the underlying database.

when should I set up ngrok for twilio

Use `ngrok` during local development to create a publicly accessible URL for your RedwoodJS API. This allows Twilio to send status callbacks to your local machine for testing before deployment.

how to handle twilio callback errors

Wrap your callback handler logic in a `try...catch` block to handle potential database errors or missing parameters. Log errors for debugging, but return a `200 OK` response to Twilio even if errors occur after signature validation to prevent excessive retries.

what is the x-twilio-signature header

The `x-twilio-signature` header in Twilio webhook requests contains a cryptographic signature of the request. This signature is used to validate the request's authenticity and ensure it came from Twilio, preventing unauthorized access.

how to test twilio status callbacks locally

Use a tool like `ngrok` to create a temporary public URL that forwards requests to your local development server. Configure this URL as your `statusCallback` URL in Twilio, enabling you to test webhook handling locally.