code examples
code examples
Build SMS Marketing Campaigns with Twilio, Node.js, and Next.js
Complete guide to building SMS marketing campaigns using Twilio Programmable Messaging API, Next.js API routes, and Node.js. Learn setup, A2P 10DLC compliance, rate limits, and best practices.
Build SMS Marketing Campaigns with Twilio, Node.js, and Next.js
What Will You Build?
You need a powerful SMS marketing platform? This guide shows you how to build one using Twilio's Programmable Messaging API integrated with Next.js and Node.js. You'll create a system capable of sending personalized SMS campaigns to your customer base, tracking delivery status, and handling responses – all within a modern, scalable web application.
SMS marketing remains one of the most direct and effective ways to reach your audience. With open rates exceeding 90%, SMS outperforms email and many other communication channels. This tutorial walks you through building a production-ready SMS marketing campaign system from the ground up.
Why Choose This Technology Stack?
Technologies Used:
- Next.js: A React framework offering server-side rendering (SSR), static site generation (SSG), and API routes for backend logic. This makes it ideal for building full-stack web applications without needing a separate backend server. Supports Next.js 14+ (requires Node.js 18.17+) or Next.js 15+ (requires Node.js 18.18+).
- Node.js: The underlying JavaScript runtime environment for Next.js. Minimum version: 18.17+ for Next.js 14, or 18.18+ for Next.js 15 (as of January 2025).
- Twilio Programmable Messaging: Twilio's SMS API provides reliable, global SMS delivery with robust features for managing messages, handling delivery status, and more. You'll use their Node.js helper library (current version: 5.x, latest 5.10.1 as of January 2025).
Sources: Next.js 14 release notes, Next.js 15 documentation, npm twilio package registry (January 2025)
Why This Combination?
- Unified Codebase: Next.js API routes allow you to write both your frontend and backend logic in JavaScript/TypeScript within the same project.
- Rapid Development: Next.js's file-based routing and API route structure speeds up development.
- Scalability: Both Next.js and Twilio scale easily to handle growing user bases and message volumes.
- Developer Experience: The Twilio Node.js library provides a clean, well-documented interface for interacting with their API.
What You'll Need Before Starting
Prerequisites:
- Node.js: Version 18.17+ (for Next.js 14) or 18.18+ (for Next.js 15) installed. Node.js 20 LTS or 22 LTS recommended for production (January 2025).
- npm or yarn: Package managers for installing dependencies.
- Twilio Account: Sign up for a free trial at https://www.twilio.com/try-twilio. Trial accounts include credits for testing.
- A2P 10DLC Registration (US SMS): If sending marketing SMS to US numbers, you must complete A2P 10DLC registration. Unregistered US-bound 10DLC traffic is blocked (enforced since August 31, 2023) and incurs additional carrier fees.
Source: Twilio A2P 10DLC compliance documentation (August 2023 enforcement)
Twilio Resources You'll Need:
- Phone Number: A Twilio phone number capable of sending SMS. You can purchase one from the Twilio Console. Toll-free numbers work well for marketing campaigns in the US and Canada.
- Account SID and Auth Token: Your Twilio credentials, found in your Twilio Console Dashboard.
- Messaging Service (Recommended): A Messaging Service provides features like sender pool management, link shortening, and automatic failover – essential for professional campaigns.
FAQ
What is A2P 10DLC and do I need it?
A2P 10DLC (Application-to-Person 10-Digit Long Code) is a US SMS registration system for businesses sending SMS to US phone numbers. If you're sending marketing, promotional, or any application-generated messages to US numbers, registration is mandatory since August 31, 2023. Unregistered traffic is blocked by carriers and incurs additional fees.
How many messages per second can I send?
Rate limits vary by sender type: US toll-free numbers default to 3 MPS (can be increased with approval), international numbers typically support 10 MPS, and US short codes for MMS support 50 MPS. Your actual limit depends on A2P 10DLC campaign registration for US long codes. Never use multiple numbers to artificially increase throughput (snowshoeing) – this results in carrier filtering.
What's the difference between using a phone number and a Messaging Service?
A Messaging Service provides sender pool management, sticky sender (consistent sender per recipient), automatic failover, opt-out handling, and geo-match routing. It's highly recommended over hardcoding a single phone number as it scales better and handles edge cases automatically without code changes.
How do status callbacks work?
When you provide a statusCallback URL, Twilio sends HTTP POST requests to that endpoint as message status changes (queued → sent → delivered or failed). You can append custom query parameters to track your internal IDs (userId, campaignId). Twilio queues messages for up to 10 hours per sender.
What happens if a message fails to send?
Your status callback endpoint receives the failure notification with an error code. Common failure reasons include invalid phone numbers, carrier blocking, or the recipient's phone being off. Implement error handling in your status callback to log failures and potentially retry or alert administrators.
Can I send MMS (images/videos) with this setup?
Yes. Twilio Messaging Services support MMS. Add a mediaUrl parameter to your client.messages.create() call with an array of publicly accessible image/video URLs. Note that MMS has different rate limits (50 MPS for US short codes) and pricing than SMS.
How do I handle unsubscribe requests?
Include opt-out instructions in your messages (e.g., "Reply STOP to unsubscribe"). Configure your Messaging Service to handle standard opt-out keywords (STOP, UNSUBSCRIBE) automatically, or build a reply handler at /api/sms-reply to process incoming messages and update your unsubscribe list in your database.
What's the cost per SMS message?
Pricing varies by destination country and sender type. US toll-free messages typically cost $0.0075–$0.0095 per segment, US 10DLC costs $0.0079 per segment, and international rates vary widely. Check Twilio's pricing page for your specific destinations. Messages over 160 characters split into multiple segments.
1. Setting Up the Project
Let's initialize our Next.js project and install the necessary dependencies.
-
Create a New Next.js App: Open your terminal and run the following command. Replace
twilio-sms-senderwith your desired project name.bashnpx create-next-app@latest twilio-sms-senderFollow the prompts (using defaults like TypeScript: No, ESLint: Yes, Tailwind CSS: No,
src/directory: No, App Router: No (or Yes, adapting API route location), import alias: No is fine for this guide). -
Navigate into Project Directory:
bashcd twilio-sms-sender -
Install Twilio Node.js Helper Library: This library simplifies interaction with the Twilio API. The current major version is 5.x (latest 5.10.1 as of January 2025).
bashnpm install twiliobash# or using yarn: yarn add twilioNote: Twilio Node SDK v5 contains breaking changes from v4. If upgrading from v4, review the migration guide. Version 5 features improved TypeScript support, 31% smaller bundle size, and lazy loading.
Source: Twilio Node.js SDK GitHub releases, npm package registry (January 2025)
-
Create Environment Variables File: We'll store our sensitive Twilio credentials and configuration here. Create a file named
.env.localin the root of your project.bashtouch .env.local- Why
.env.local? Next.js automatically loads variables from this file intoprocess.env. It's listed in the default.gitignore, preventing accidental commits of sensitive keys.
- Why
-
Add Environment Variables to
.env.local: Open.env.localand add the following lines. We'll get the actual values in the next section.dotenv# .env.local TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID TWILIO_AUTH_TOKEN=YOUR_AUTH_TOKEN # Recommended: Use a Messaging Service SID for scalability TWILIO_MESSAGING_SERVICE_SID=YOUR_MESSAGING_SERVICE_SID # Optional but recommended: Define a base URL for status callbacks # Example: Your deployed app's URL or ngrok URL for local testing STATUS_CALLBACK_BASE_URL=http://localhost:3000- Purpose:
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN: Credentials for authenticating with the Twilio API.TWILIO_MESSAGING_SERVICE_SID: Identifier for a Twilio Messaging Service, which manages sender pools (phone numbers, short codes, Alphanumeric Sender IDs), opt-out handling, and other features. Using this is highly recommended over hardcoding a singlefromnumber.STATUS_CALLBACK_BASE_URL: The base URL where your application will be accessible to receive status updates from Twilio.
- Purpose:
-
Project Structure: Your relevant project structure should look like this:
twilio-sms-sender/ ├── pages/ │ ├── api/ │ │ └── # We will create send-sms.js here │ ├── _app.js │ └── index.js ├── public/ ├── .env.local # Your environment variables ├── .gitignore ├── package.json ├── next.config.js └── README.md
2. Configuring Twilio Credentials and Sender
Before writing code, you need your Twilio credentials and a way to send messages (a Messaging Service SID is recommended).
-
Log in to Twilio Console: Go to https://www.twilio.com/console and log in.
-
Find Account SID and Auth Token: On your main dashboard, you'll find your
ACCOUNT SIDandAUTH TOKEN. -
Update
.env.local: Copy yourACCOUNT SIDandAUTH TOKENand paste them as the values forTWILIO_ACCOUNT_SIDandTWILIO_AUTH_TOKENin your.env.localfile.- Security: Keep these credentials secret. Never commit them directly into your code or version control. Environment variables are the standard way to handle this.
-
Set up a Messaging Service (Recommended):
- Navigate to Messaging > Services in the Twilio Console sidebar.
- Click Create Messaging Service.
- Give your service a friendly name (e.g., "NextJS Marketing Campaign Service").
- Select a use case – "Marketing" is appropriate here. Click Create Messaging Service.
- Add Senders: On the service's configuration page, click Add Senders. Choose a sender type (e.g., Phone Number). If you don't have a Twilio number, you'll be prompted to buy one. Add at least one SMS-enabled Twilio phone number to the sender pool.
- Why a Service? It handles sender selection logic (useful if you have multiple numbers), sticky sender (replies come from the same number), short code/toll-free number management, alphanumeric sender IDs, geo-match routing, opt-out management, and more advanced features without changing your core sending code.
- Copy Messaging Service SID: Once created, find the SID (starting with
MG...) on the Messaging Service's properties page. - Update
.env.local: Paste this SID as the value forTWILIO_MESSAGING_SERVICE_SIDin your.env.localfile.
⚠️ A2P 10DLC Compliance (US Marketing SMS):
If you're sending marketing or promotional SMS to US phone numbers, you must register for A2P 10DLC:
- Brand Registration: Provide information about your organization.
- Campaign Registration: Describe your use case, opt-in/opt-out methods, and message content.
- Enforcement: Since August 31, 2023, Twilio blocks all unregistered US-bound 10DLC messages. Unregistered traffic incurs additional carrier fees.
- Registration Location: Register within the Twilio Console under Messaging > Regulatory Compliance > A2P 10DLC.
Benefits: Improved deliverability, reduced filtering risks, higher throughput (varies by campaign type), and compliance with US carrier requirements.
Source: Twilio A2P 10DLC compliance documentation (August 2023 enforcement date)
-
Set Callback URL (Optional but Recommended):
- While still in the Messaging Service configuration, go to the Integration section.
- In the Incoming Messages settings, ensure Send an incoming message webhook is selected under ""Request URL"". Set the webhook URL if you intend to handle replies (e.g.,
YOUR_DEPLOYED_URL/api/sms-reply). - In the Status Callback URL field under Delivery Status Callback, enter the URL where Twilio should send delivery status updates. We'll construct this dynamically in our code later, but you can set a default here if preferred. It typically points to another API endpoint you'll create (e.g.,
YOUR_DEPLOYED_URL/api/sms-status). Setting it per-message provides more flexibility.
3. Building the API Endpoint (send-sms)
Now, let's create the Next.js API route that will handle incoming requests to send SMS messages.
-
Create the API Route File: Inside the
pages/api/directory, create a new file namedsend-sms.js. -
Basic API Route Structure: Add the following boilerplate code to
pages/api/send-sms.js:javascript// pages/api/send-sms.js // Import the Twilio helper library import twilio from 'twilio'; export default async function handler(req, res) { // We only want to handle POST requests if (req.method !== 'POST') { res.setHeader('Allow', 'POST'); return res.status(405).json({ message: 'Method Not Allowed' }); } // Logic will be added in the next steps... // Placeholder for now return res.status(501).json({ message: 'Not Implemented Yet' }); }- Explanation: This sets up a standard Next.js API route handler. It currently checks if the request method is POST, as we'll be sending data (phone number, message) in the request body.
4. Implementing SMS Sending Logic
Let's add the core logic to interact with the Twilio API.
-
Import and Initialize Twilio Client: Modify
pages/api/send-sms.jsto load credentials and initialize the client outside the handler function for efficiency:javascript// pages/api/send-sms.js import twilio from 'twilio'; // Load credentials from environment variables const accountSid = process.env.TWILIO_ACCOUNT_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; const messagingServiceSid = process.env.TWILIO_MESSAGING_SERVICE_SID; const statusCallbackBaseUrl = process.env.STATUS_CALLBACK_BASE_URL; // Base URL for callbacks // Ensure required environment variables are set during initialization if (!accountSid || !authToken || !messagingServiceSid) { console.error(""Twilio credentials or Messaging Service SID are not configured in environment variables.""); // This check runs at server start/build time. Handle runtime check if needed. } // Create an authenticated Twilio client (only if credentials exist) const client = (accountSid && authToken) ? twilio(accountSid, authToken) : null; export default async function handler(req, res) { if (req.method !== 'POST') { res.setHeader('Allow', 'POST'); return res.status(405).json({ message: 'Method Not Allowed' }); } // Check if client was initialized successfully if (!client || !messagingServiceSid) { console.error(""Twilio client not initialized or Messaging Service SID missing.""); return res.status(500).json({ message: 'Internal Server Error: Configuration missing.' }); } // Rest of the logic goes here... } -
Extract Data from Request Body: Inside the
handlerfunction, after the client check, add:javascript// pages/api/send-sms.js // ... (inside handler function) const { to, body, customData } = req.body; // Expect 'to', 'body', and optional 'customData' // Basic validation if (!to || !body) { return res.status(400).json({ message: 'Missing required parameters: ""to"" and ""body"".' }); } // More robust phone number validation is recommended in production // Example: Use a library like 'google-libphonenumber' // For simplicity, we'll check for a basic pattern (E.164 format is expected by Twilio) const phoneRegex = /^\+[1-9]\d{1,14}$/; // Basic E.164 check if (!phoneRegex.test(to)) { return res.status(400).json({ message: 'Invalid ""to"" phone number format. Use E.164 format (e.g., +1234567890).' }); }- Explanation: We extract the recipient
tonumber and the messagebodyfrom the JSON payload of the POST request. We also add basic validation to ensure they exist and the phone number looks somewhat like the E.164 format Twilio requires (+followed by country code and number). We also allow for optionalcustomDatawhich we'll use for the status callback.
- Explanation: We extract the recipient
-
Call the Twilio API: Add the following
try...catchblock after the validation logic inside thehandlerfunction:javascript// pages/api/send-sms.js // ... (inside handler function, after validation) try { // Construct the status callback URL with custom data let statusCallbackUrl = null; if (statusCallbackBaseUrl) { const callbackEndpoint = '/api/sms-status'; // Define your status handling endpoint statusCallbackUrl = new URL(callbackEndpoint, statusCallbackBaseUrl); // Append custom data as query parameters (if provided) if (customData && typeof customData === 'object') { Object.keys(customData).forEach(key => { // Ensure values are strings for URL parameters statusCallbackUrl.searchParams.append(key, String(customData[key])); }); } // You might always want to include a message SID placeholder or internal ID // statusCallbackUrl.searchParams.append('internalMsgId', 'some-unique-id'); } const message = await client.messages.create({ to: to, // Recipient phone number messagingServiceSid: messagingServiceSid, // Use the Messaging Service SID body: body, // The text message content // Provide the dynamically generated status callback URL statusCallback: statusCallbackUrl ? statusCallbackUrl.toString() : undefined, // You can specify specific events, e.g., ['sent', 'delivered', 'failed'] // statusCallbackEvent: ['delivered', 'failed'], }); // Send success response return res.status(200).json({ success: true, messageSid: message.sid, status: message.status, // Initial status (usually 'queued' or 'sending') }); } catch (error) { console.error('Twilio API Error:', error); // Provide a generic error message to the client // Log the detailed error internally for debugging let errorMessage = 'Failed to send SMS.'; let statusCode = 500; // Check for specific Twilio error codes if needed for more granular client feedback // Twilio errors often have a status property if (error.status === 400) { // Example: Invalid 'To' number, invalid parameters errorMessage = `Failed to send SMS: ${error.message}`; // Twilio often provides useful messages statusCode = 400; } else if (error.code === 20003) { // Example: Authentication error errorMessage = 'Failed to send SMS due to authentication error.'; statusCode = 500; // Internal config issue } else if (error.code === 21211) { // Example: Invalid 'To' number format errorMessage = `Invalid 'To' phone number: ${error.message}`; statusCode = 400; } else if (error.code === 21608) { // Example: Unverified number for trial account errorMessage = `Cannot send SMS: ${error.message}`; statusCode = 400; } // Add more specific error handling based on Twilio error codes: // https://www.twilio.com/docs/api/errors return res.status(statusCode).json({ success: false, message: errorMessage, // Avoid exposing raw error details in production errorDetails: process.env.NODE_ENV === 'development' ? error.message : undefined, errorCode: error.code || undefined // Optionally include Twilio error code }); }- Explanation:
- We wrap the API call in a
try...catchblock for error handling. - We construct the
statusCallbackUrlby combining theSTATUS_CALLBACK_BASE_URLfrom environment variables with a specific endpoint path (/api/sms-status). Crucially, we append anycustomDatareceived in the request body as query parameters to this URL. This allows us to pass identifiers (likeuserId,campaignId,internalMessageId) that Twilio will send back to our status handler, enabling us to correlate the delivery status with the original sending request. We ensure custom data values are converted to strings. client.messages.create()sends the request to Twilio.to: The recipient's phone number (validated earlier).messagingServiceSid: Tells Twilio to use our configured service for sending, handling sender selection, opt-outs, etc.body: The SMS content.statusCallback: The URL Twilio will POST status updates (queued, sending, sent, delivered, failed, etc.) to. We pass the URL we constructed, including any custom query parameters.- On success, we return a success response including the
message.sid(Twilio's unique ID for the message) and its initialstatus. - On error, we log the detailed error and return an appropriate error status code and message to the client, checking for common Twilio error codes. We avoid leaking detailed error info in production environments but optionally include the Twilio error code.
- We wrap the API call in a
- Explanation:
5. Understanding Status Callbacks
We configured the statusCallback parameter, but what does it do?
- Purpose: When the status of your sent SMS changes (e.g., from
senttodeliveredorfailed), Twilio makes an HTTP POST request to thestatusCallbackURL you provided. - Data Received: The request body from Twilio contains details about the message, including its
MessageSid, the updatedMessageStatus, error codes if it failed, and importantly, any query parameters you appended to the statusCallback URL. - Implementation (Separate Guide): Building the
/api/sms-statusendpoint to receive these callbacks, parse the data (especially your custom parameters likeuserId), and update your application's state (e.g., marking a campaign message as delivered in a database) is a critical next step for production systems but is beyond the scope of this sending guide. You would typically:- Create a new API route (e.g.,
pages/api/sms-status.js). - Verify the request comes from Twilio (using Request Validation).
- Parse the request body (which is form-urlencoded, e.g., using
req.bodyin Next.js). - Extract the
MessageSid,MessageStatus, and your custom query parameters (e.g.,req.query.userId,req.query.campaignId- note they come from the query string we built). - Update your database or internal state based on this information.
- Respond to Twilio with a
200 OKstatus (or an empty<Response/>TwiML).
- Create a new API route (e.g.,
6. Input Validation & Basic Error Handling (Included Above)
We've already incorporated basic validation and error handling:
- Method Check: Ensures only POST requests are processed.
- Parameter Check: Verifies that
toandbodyare present in the request. - Format Check: Basic E.164 format validation for the
tonumber. Consider using a dedicated library likegoogle-libphonenumberfor robust, international validation in production. - Try/Catch Block: Catches errors during the Twilio API call.
- Error Logging: Logs detailed errors server-side (
console.error). - User-Friendly Error Responses: Returns generic error messages to the client in production, while potentially showing more detail in development. Includes appropriate HTTP status codes (400 for client errors, 500 for server/Twilio errors) and potentially Twilio error codes.
7. Testing the Endpoint
You can test your API endpoint locally using curl or a tool like Postman.
-
Start the Next.js Development Server:
bashnpm run devbash# or yarn devYour app will typically start on
http://localhost:3000. -
Send a Test Request using
curl: Open a new terminal window and run the following command. You must replace+15551234567with a real, verified phone number (like your own cell phone if using a Twilio trial account) and adjust the message.bashcurl -X POST http://localhost:3000/api/send-sms \ -H "Content-Type: application/json" \ -d '{ "to": "+15551234567", "body": "Hello from Next.js and Twilio! This is a test message.", "customData": { "userId": "user-abc-123", "campaignId": "spring-sale-2025" } }' -
Expected Success Response (JSON):
json{ "success": true, "messageSid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "status": "queued" }You should also receive the SMS on the target phone number shortly after. Check the terminal running the Next.js dev server for logs.
-
Example Error Response (e.g., Missing 'to' number):
bash# Request: curl -X POST http://localhost:3000/api/send-sms \ -H "Content-Type: application/json" \ -d '{ "body": "This will fail." }' # Response (Status Code 400): { "message": "Missing required parameters: \"to\" and \"body\"." } -
Example Error Response (e.g., Invalid Twilio Credentials):
json// Status Code 500 { "success": false, "message": "Failed to send SMS due to authentication error.", "errorCode": 20003 // "errorDetails": "Authenticate" // (Only shown in development if configured) }Check the server logs for the detailed Twilio error message.
8. Deployment (Example: Vercel)
Deploying a Next.js application with API routes is straightforward with platforms like Vercel.
-
Push Code to Git: Ensure your code (excluding
.env.localandnode_modules/) is committed and pushed to a Git provider (GitHub, GitLab, Bitbucket). -
Import Project on Vercel:
- Sign up or log in to Vercel.
- Click Add New... > Project.
- Import the Git repository containing your project.
-
Configure Environment Variables:
- During import or in the Project Settings > Environment Variables section, add the same environment variables you defined in
.env.local:TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKENTWILIO_MESSAGING_SERVICE_SIDSTATUS_CALLBACK_BASE_URL(Set this to your main Vercel deployment URL, e.g.,https://your-project-name.vercel.app)
- Ensure they are set for ""Production"", ""Preview"", and ""Development"" environments as needed. Crucially, use your actual production Twilio credentials here.
- During import or in the Project Settings > Environment Variables section, add the same environment variables you defined in
-
Deploy: Vercel will typically auto-detect the Next.js framework and build/deploy your application.
-
Update Twilio Callback URLs (If Necessary): If you hardcoded
localhostURLs in the Twilio console for your Messaging Service's status or incoming webhooks, update them to use your production Vercel URL (e.g.,https://your-project-name.vercel.app/api/sms-status). Using the dynamically generatedstatusCallbackURL in the code avoids needing to update Twilio for that specific callback each time, as long as theSTATUS_CALLBACK_BASE_URLenvironment variable is correct on Vercel.
9. Verification Checklist
- Node.js (v18+) and npm/yarn are installed.
- Next.js project created (
npx create-next-app). -
twilionpm package installed. -
.env.localfile created and added to.gitignore. - Twilio Account SID and Auth Token obtained from Twilio Console.
- Twilio Messaging Service created and SID obtained (Recommended).
- Environment variables (
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_MESSAGING_SERVICE_SID,STATUS_CALLBACK_BASE_URL) correctly set in.env.local. - API route
pages/api/send-sms.jscreated. - API route correctly initializes the Twilio client using environment variables.
- API route handles POST requests only.
- API route extracts and validates
toandbodyparameters. - API route constructs
statusCallbackURL with custom data from request body. - API route calls
client.messages.createwithto,body,messagingServiceSid, andstatusCallback. - API route includes
try...catchblock for error handling. - Appropriate success (200) and error (4xx, 500) JSON responses are returned.
- Tested locally using
curlor Postman with valid and invalid inputs. - SMS successfully received on the test phone number.
- Deployed to a hosting provider (e.g., Vercel).
- Environment variables correctly configured on the hosting provider.
- Tested the deployed endpoint.
10. Troubleshooting & Caveats
-
Invalid Credentials (Twilio Error 20003): Double-check
TWILIO_ACCOUNT_SIDandTWILIO_AUTH_TOKENin your.env.localfile and your deployment environment variables. Ensure there are no extra spaces or characters. Restart the server after changes. -
Unverified 'To' Number (Trial Accounts - Twilio Error 21608): If using a Twilio trial account, you can only send SMS to phone numbers verified in your Twilio Console under Phone Numbers > Manage > Verified Caller IDs.
-
Invalid 'To' Phone Number Format (Twilio Error 21211): Ensure the
tonumber is in E.164 format (e.g.,+16505551234). Use a library for robust validation. -
Messaging Service SID Not Set: If
TWILIO_MESSAGING_SERVICE_SIDis missing or incorrect, the API call might fail, or Twilio might default to a specific number, which might not be desired. Ensure it's correctly set in environment variables. -
Environment Variables Not Loaded:
- Local: Make sure the file is named
.env.localand is in the project root. Restart your development server (npm run dev) after changing.env.local. - Deployment (Vercel): Ensure variables are set correctly in the Vercel project settings and that a new deployment was triggered after adding/updating them. Check Vercel deployment logs.
- Local: Make sure the file is named
-
Status Callbacks Not Firing:
- Verify the
STATUS_CALLBACK_BASE_URLis correct and publicly accessible (use your Vercel URL, notlocalhost, when deployed). - Ensure the endpoint you specified (e.g.,
/api/sms-status) actually exists, is deployed, and can handle POST requests. - Check the Twilio Debugger logs in the console for errors related to webhook requests (e.g., 404 Not Found, 500 Server Error from your endpoint).
- Remember Twilio needs to be able to reach the URL from the public internet.
localhostonly works if using a tunneling service likengrokand providing the ngrok URL as the base URL.
- Verify the
-
Rate Limiting (Twilio Error 20429): High volumes of API requests can trigger rate limits. Implement retry logic with exponential backoff for production systems.
Rate Limits by Sender Type (as of January 2025):
- US Toll-Free: Default 3 MPS (messages per second), can be increased with approval
- US Long Codes: Varies by A2P 10DLC registration outcome and campaign type
- International (outside US/Canada): Typically 10 MPS
- US Short Codes (MMS): 50 MPS limit
- API Concurrency: 429 responses during high usage indicate concurrency limits exceeded
Best Practices:
- Do NOT use multiple long codes or toll-free numbers to artificially increase throughput (known as "snowshoeing"). This results in carrier filtering.
- Do use Messaging Services to queue messages automatically. Twilio queues up to 10 hours of messages per sender.
- Do implement exponential backoff for 429 responses.
- Do consider upgrading to higher-throughput sender types (short codes, A2P 10DLC approved campaigns) for large volumes.
Source: Twilio rate limits documentation, Best Practices for Scaling with Messaging Services (January 2025)
-
SMS Character Limits & Encoding: Standard SMS messages have limits (160 GSM-7 characters, fewer for UCS-2 encoding if using special characters/emojis). Longer messages are split (concatenated SMS), consuming more message segments and potentially costing more. Be mindful of message length.
-
Compliance (A2P 10DLC, etc.): Sending Application-to-Person (A2P) SMS, especially in the US, requires registration (A2P 10DLC). Ensure you comply with all relevant regulations and Twilio's Messaging Policies. Using a Messaging Service helps manage compliance aspects. Registration is mandatory as of August 31, 2023 for US-bound 10DLC traffic.
11. Next Steps & Conclusion
You've successfully built a foundational Next.js API endpoint for sending SMS messages with Twilio!
Potential Enhancements:
- Build the Status Callback Handler: Create the
/api/sms-statusendpoint to receive delivery updates from Twilio, validate the requests, parse the data (including your custom parameters), and update your application state/database. - Add Request Validation: Implement Twilio Request Validation for your status callback endpoint to ensure requests genuinely come from Twilio.
- Database Integration: Store campaign details, message logs, recipient lists, and delivery statuses in a database (e.g., PostgreSQL, MongoDB) accessible from your API routes.
- Frontend UI: Create a simple React component within your Next.js app to provide a form for entering recipient numbers and messages, which calls your
/api/send-smsendpoint. - Advanced Error Handling & Retries: Implement more robust error handling and automatic retry mechanisms (e.g., using queues or libraries like
async-retry) for transient network or API issues. - Input Validation Library: Use a dedicated library (
google-libphonenumber,zod,joi) for stricter validation of phone numbers and request payloads. - Authentication/Authorization: Protect your API endpoint to ensure only authorized users or systems can trigger SMS sends.
- Rate Limiting: Implement rate limiting on your API endpoint to prevent abuse.
- Unit & Integration Testing: Add automated tests for your API route logic.
This guide provides a solid starting point for integrating Twilio SMS capabilities into your Next.js projects, enabling powerful communication features for marketing or notifications. Remember to explore Twilio's extensive documentation for more advanced features and best practices.
Frequently Asked Questions
How to send SMS messages with Next.js and Twilio?
Create a Next.js API route that uses the Twilio Node.js helper library to send SMS messages. This involves setting up a dedicated endpoint ('/api/send-sms') to handle incoming requests, extracting recipient and message details, and calling the Twilio API to send the message.
What is a Twilio Messaging Service and why use it?
A Twilio Messaging Service is a tool that simplifies sending SMS messages by managing sender pools, opt-out handling, and other features. It's recommended over hardcoding a single 'from' number for better scalability and easier management of multiple senders or shortcodes.
How to set up Twilio status callbacks in Next.js?
Provide a statusCallback URL when creating a message with the Twilio API. This URL should point to an API endpoint in your Next.js application (e.g., '/api/sms-status'). Twilio will send POST requests to this URL with delivery status updates.
How to handle Twilio status callbacks in my Next js app?
Create a separate API route (e.g., '/api/sms-status') to handle incoming status updates from Twilio. This endpoint should parse the request body, extract the message status, and any custom data you included, then update your application's internal state accordingly.
Why use Next.js for sending SMS with Twilio?
Next.js simplifies full-stack development by allowing frontend and backend (API routes) to coexist. It streamlines development and deployment, especially with serverless platforms like Vercel.
What is the purpose of .env.local file in Next.js Twilio SMS project?
The `.env.local` file stores sensitive information like your Twilio Account SID, Auth Token, and Messaging Service SID. Next.js loads these as environment variables, keeping them secure and out of version control.
How to install the Twilio helper library for Node.js?
Use your preferred package manager (npm or yarn). Run `npm install twilio` or `yarn add twilio` in your project's root directory to install the library.
What environment variables are needed for Twilio SMS integration?
You need `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, and ideally `TWILIO_MESSAGING_SERVICE_SID`. A `STATUS_CALLBACK_BASE_URL` is also recommended for handling delivery status updates.
Where can I find my Twilio Account SID and Auth Token?
Log in to the Twilio Console ([https://www.twilio.com/console](https://www.twilio.com/console)). Your Account SID and Auth Token are displayed on your main dashboard.
How to test my Next.js Twilio SMS API endpoint?
Use tools like `curl` or Postman to send POST requests to your local or deployed API endpoint. Provide the recipient phone number and message body in JSON format. Ensure the 'to' number is valid and verified if using a trial account.
What is the E.164 format for phone numbers?
E.164 is an international standard for phone number formatting. It starts with a '+' followed by the country code and the national subscriber number. For example: +1234567890. Twilio expects numbers in E.164 format.
How to handle errors when sending SMS messages with Twilio?
Wrap your Twilio API call in a try...catch block. Log the error for debugging and return appropriate error messages and status codes to the client. Check for specific Twilio error codes for better error handling.
When should I use a Messaging Service for sending Twilio SMS?
A Messaging Service is recommended for almost all production use cases involving sending SMS. It improves scalability, provides features like sender selection and opt-out management, and simplifies managing multiple phone numbers or short codes.
Can I send SMS messages to unverified numbers with a Twilio trial account?
No. Trial accounts can only send SMS to phone numbers you've verified in your Twilio console under 'Verified Caller IDs'. Verify your own number for testing.
How to deploy a Next.js Twilio SMS application to Vercel?
Push your code (excluding .env.local) to a Git repository. Import the project into Vercel. Configure the same environment variables as in .env.local on Vercel, ensuring they're set for the relevant environments. Trigger a new deployment after configuring the environment variables.