How to Integrate Twilio SMS in 2026
Twilio is the most widely used SMS API. This guide covers everything: sending messages, receiving replies, phone number verification, and handling delivery reports.
What You'll Build
- Send SMS messages programmatically
- Receive and respond to incoming SMS
- Phone number verification (OTP)
- Delivery status tracking via webhooks
- MMS (picture messages)
Prerequisites: Node.js 18+, Twilio account (free trial includes $15 credit).
1. Setup
Create a Twilio Account
- Sign up at twilio.com
- Get a phone number (free with trial)
- Copy your Account SID, Auth Token, and phone number
Install
npm install twilio
Initialize
// lib/twilio.ts
import twilio from 'twilio';
export const twilioClient = twilio(
process.env.TWILIO_ACCOUNT_SID!,
process.env.TWILIO_AUTH_TOKEN!
);
export const TWILIO_PHONE = process.env.TWILIO_PHONE_NUMBER!;
Environment Variables
# .env.local
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=your_auth_token
TWILIO_PHONE_NUMBER=+15551234567
2. Send SMS
Basic Message
import { twilioClient, TWILIO_PHONE } from '@/lib/twilio';
const message = await twilioClient.messages.create({
body: 'Your order #1234 has shipped! Track it here: https://track.example.com/1234',
from: TWILIO_PHONE,
to: '+15559876543',
});
console.log('Message SID:', message.sid);
console.log('Status:', message.status); // 'queued'
API Route (Next.js)
// app/api/sms/send/route.ts
import { NextResponse } from 'next/server';
import { twilioClient, TWILIO_PHONE } from '@/lib/twilio';
export async function POST(req: Request) {
const { to, message } = await req.json();
// Validate phone number format
if (!/^\+\d{10,15}$/.test(to)) {
return NextResponse.json(
{ error: 'Invalid phone number format. Use E.164: +15551234567' },
{ status: 400 }
);
}
try {
const result = await twilioClient.messages.create({
body: message,
from: TWILIO_PHONE,
to,
});
return NextResponse.json({
sid: result.sid,
status: result.status,
});
} catch (error: any) {
return NextResponse.json(
{ error: error.message },
{ status: error.status || 500 }
);
}
}
3. Receive SMS
Set Up Webhook
In Twilio Console → Phone Numbers → Your Number → Messaging:
- When a message comes in:
https://your-server.com/api/sms/receive - HTTP Method: POST
Handle Incoming Messages
// app/api/sms/receive/route.ts
import { NextResponse } from 'next/server';
import twilio from 'twilio';
export async function POST(req: Request) {
const formData = await req.formData();
const from = formData.get('From') as string;
const body = formData.get('Body') as string;
const messageSid = formData.get('MessageSid') as string;
console.log(`SMS from ${from}: ${body}`);
// Process the message (save to DB, trigger action, etc.)
await processIncomingSMS({ from, body, messageSid });
// Reply with TwiML
const twiml = new twilio.twiml.MessagingResponse();
twiml.message('Thanks for your message! We\'ll get back to you shortly.');
return new Response(twiml.toString(), {
headers: { 'Content-Type': 'text/xml' },
});
}
Auto-Responder
const twiml = new twilio.twiml.MessagingResponse();
const lowerBody = body.toLowerCase().trim();
if (lowerBody === 'status') {
twiml.message('Your order is on its way! ETA: Tomorrow by 5pm.');
} else if (lowerBody === 'help') {
twiml.message('Commands:\nSTATUS - Check order\nSTOP - Unsubscribe\nHELP - This menu');
} else if (lowerBody === 'stop') {
twiml.message('You have been unsubscribed. Reply START to re-subscribe.');
await unsubscribeUser(from);
} else {
twiml.message('Reply HELP for available commands.');
}
4. Phone Number Verification (OTP)
Using Twilio Verify
npm install twilio # Already installed
Send Verification Code
// app/api/verify/send/route.ts
import { NextResponse } from 'next/server';
import { twilioClient } from '@/lib/twilio';
const VERIFY_SERVICE_SID = process.env.TWILIO_VERIFY_SERVICE_SID!;
export async function POST(req: Request) {
const { phoneNumber } = await req.json();
const verification = await twilioClient.verify.v2
.services(VERIFY_SERVICE_SID)
.verifications.create({
to: phoneNumber,
channel: 'sms', // or 'call', 'email', 'whatsapp'
});
return NextResponse.json({
status: verification.status, // 'pending'
});
}
Check Verification Code
// app/api/verify/check/route.ts
import { NextResponse } from 'next/server';
import { twilioClient } from '@/lib/twilio';
const VERIFY_SERVICE_SID = process.env.TWILIO_VERIFY_SERVICE_SID!;
export async function POST(req: Request) {
const { phoneNumber, code } = await req.json();
const check = await twilioClient.verify.v2
.services(VERIFY_SERVICE_SID)
.verificationChecks.create({
to: phoneNumber,
code,
});
if (check.status === 'approved') {
// Mark phone as verified in your database
await markPhoneVerified(phoneNumber);
return NextResponse.json({ verified: true });
}
return NextResponse.json({ verified: false }, { status: 400 });
}
Verification UI
'use client';
import { useState } from 'react';
export function PhoneVerification() {
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [step, setStep] = useState<'phone' | 'code' | 'verified'>('phone');
const sendCode = async () => {
await fetch('/api/verify/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phoneNumber: phone }),
});
setStep('code');
};
const checkCode = async () => {
const res = await fetch('/api/verify/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phoneNumber: phone, code }),
});
const data = await res.json();
if (data.verified) setStep('verified');
};
if (step === 'verified') return <p>✅ Phone verified!</p>;
return (
<div>
{step === 'phone' && (
<>
<input
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="+15551234567"
/>
<button onClick={sendCode}>Send Code</button>
</>
)}
{step === 'code' && (
<>
<input
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter 6-digit code"
maxLength={6}
/>
<button onClick={checkCode}>Verify</button>
</>
)}
</div>
);
}
5. Delivery Status Webhooks
Configure Status Callback
const message = await twilioClient.messages.create({
body: 'Your appointment is tomorrow at 2pm.',
from: TWILIO_PHONE,
to: '+15559876543',
statusCallback: 'https://your-server.com/api/sms/status',
});
Handle Status Updates
// app/api/sms/status/route.ts
export async function POST(req: Request) {
const formData = await req.formData();
const messageSid = formData.get('MessageSid') as string;
const status = formData.get('MessageStatus') as string;
const errorCode = formData.get('ErrorCode') as string;
// Status values: queued → sent → delivered (or failed/undelivered)
await updateMessageStatus(messageSid, status, errorCode);
return new Response('OK');
}
Status Flow
queued → sending → sent → delivered ✅
→ undelivered ❌ (carrier rejected)
→ failed ❌ (Twilio couldn't send)
6. Send MMS (Picture Messages)
const message = await twilioClient.messages.create({
body: 'Check out this product!',
from: TWILIO_PHONE,
to: '+15559876543',
mediaUrl: ['https://your-cdn.com/product-image.jpg'],
});
MMS limitations: Only works in US and Canada. Images must be publicly accessible URLs. Max 5MB per media file.
Pricing
| Message Type | US/Canada | International |
|---|---|---|
| SMS (outbound) | $0.0079 | $0.05-$0.15 |
| SMS (inbound) | $0.0075 | Varies |
| MMS (outbound) | $0.0200 | Not available |
| Phone number | $1.15/month | Varies |
| Verify OTP | $0.05/verification | $0.05+ |
Trial account: $15 free credit. Can only send to verified numbers.
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Not using E.164 format | Messages fail to send | Always use +[country][number] format |
| Ignoring STOP/unsubscribe | Legal violations (TCPA) | Honor opt-outs automatically |
| No rate limiting | Twilio rate limits you, messages queue | Limit to 1 msg/sec per number |
| Not handling delivery failures | Silent message loss | Use status callbacks |
| Sending from trial to unverified numbers | Messages fail | Upgrade or verify recipient numbers |
| Exposing Auth Token | Account compromise | Server-side only |
Compliance: TCPA, GDPR, and Opt-Out Management
This is the section most developers skip — and then face legal problems months later when an attorney or regulator sends a letter.
In the United States, the Telephone Consumer Protection Act (TCPA) requires explicit prior consent before sending marketing or promotional SMS to any consumer. The standard for what counts as "explicit consent" has been tightened through FCC rulemaking, and the penalties for non-compliance are steep: $500-$1,500 per violation, and violations can be aggregated in class action suits. Transactional messages — order confirmations, shipping notifications, appointment reminders, one-time passwords — have lighter consent requirements, but you still cannot send any message to a number that has opted out.
Twilio automatically handles the standard opt-out keywords (STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT) on US and Canadian numbers. When a user replies "STOP", Twilio marks that number as opted out at the account level and will not deliver future messages from your account to that number. The problem is that Twilio's opt-out state is separate from your application's database — so you need to sync the two. Set up the opt-out webhook:
// In Twilio Console > Messaging > Settings > Opt-Out Management
// Set webhook URL: https://your-server.com/api/sms/opt-out
export async function POST(req: Request) {
const formData = await req.formData();
const to = formData.get('To') as string; // Your Twilio number
const from = formData.get('From') as string; // The user who opted out
const optOutType = formData.get('OptOutType') as string; // STOP, HELP, etc.
if (optOutType === 'STOP') {
await db.users.update({
where: { phone: from },
data: { smsOptedOut: true }
});
}
return new Response('OK');
}
For GDPR compliance in the European Union and UK: explicit documented consent is required before sending any SMS. Users must be able to withdraw consent at any time, and you must maintain records of when and how consent was given. Store a consent timestamp and source with every phone number in your database — a field like smsConsentAt and smsConsentSource (web form, checkout, app signup) is sufficient. For EU and UK numbers, Twilio's terms require that you comply with local regulations, and compliance is your responsibility, not Twilio's.
The practical implementation: check smsOptedOut before every message send in your application logic, not just in your webhook. Defense in depth — Twilio will also block the message, but catching it before the API call is cleaner and avoids the error handling path.
High-Volume Sending Patterns
Standard US long code numbers are rate-limited to one message per second. For bulk sending — appointment reminder blasts, notification campaigns, weekly updates — this rate limit means 1,000 messages take about 17 minutes. If you're sending to 10,000 users, you're looking at nearly three hours of queue time, which is a problem if those are time-sensitive messages.
There are three main solutions depending on your volume and use case.
10DLC (10-digit long code) registration is the right choice for most SaaS apps. Since 2023, US carriers require registration for any bulk messaging over a few hundred messages per month. A registered 10DLC number costs $5-10 per month and gets higher throughput than an unregistered long code. Twilio will block or throttle unregistered numbers for bulk sends — if you're sending more than a small volume of messages, registration is not optional.
Alphanumeric Sender IDs let you send messages that show "YourBrand" as the sender instead of a phone number. This improves brand recognition and reduces the chance that users mistake your message for spam. The limitation is significant: alphanumeric IDs are supported in the UK and EU but not in the US or Canada, and they're one-way only (users cannot reply). They work well for notification-heavy applications in international markets.
Short codes (5 or 6 digit numbers) are the high-throughput option — capable of 100 or more messages per second. The tradeoffs are substantial: short codes cost around $500 per month, require carrier approval with a vetting process that takes several weeks, and are US-specific. They're appropriate for large consumer apps with millions of users, not typical SaaS applications. For most startups, 10DLC registration plus queuing messages across multiple numbers covers any realistic volume requirement.
Twilio vs Alternatives: When to Switch
Twilio is the most feature-complete SMS provider and has the best documentation and developer experience in the category. That said, it's not the cheapest option, and cost becomes a meaningful factor as message volume grows.
Vonage (formerly Nexmo) offers competitive pricing on international SMS and a similar API quality. If your application has significant traffic outside the US — particularly in Europe or Asia — Vonage is worth evaluating head-to-head. The API patterns are similar enough that switching is not a major engineering effort.
Telnyx is significantly cheaper per message than Twilio, especially for international and high-volume US sending. The API is well-designed and the documentation has improved considerably. Telnyx has fewer enterprise features and a smaller support organization, but for straightforward SMS use cases the cost savings are real.
Amazon SNS is the lowest-cost option at scale if you're already in AWS, with US SMS at $0.0075 per message and no per-number fees. The tradeoff is that SNS offers no Verify service — you'd need to implement OTP delivery and verification logic yourself. It's a reasonable option for high-volume transactional messaging in an AWS-native stack.
Sinch has particularly strong coverage in emerging markets and competitive per-message pricing globally. If a meaningful portion of your users are in Southeast Asia, Latin America, or Africa, Sinch's routing can be more reliable and cheaper than Twilio in those regions.
Practical guidance: start with Twilio. The documentation quality and developer experience justify the premium when you're early and moving fast. Revisit the decision when your monthly SMS spend exceeds $500 — at that point the pricing differences across providers become significant enough to warrant a proper evaluation.
Monitoring SMS Delivery in Production
Twilio's delivery status callbacks are the production-grade way to track whether messages actually reach users. When you send an SMS, Twilio immediately returns a message SID. The final delivery status — delivered, failed, or undelivered — arrives asynchronously via a webhook to your StatusCallback URL. A message returning status: queued means Twilio accepted it; status: delivered means the carrier confirmed receipt by the handset.
Configure StatusCallback for every outgoing message, or set a default at the messaging service level. Parse the callback and log the SID, status, and error code alongside the original message record. Twilio's error codes tell you why a message failed: error 30003 means the number is unreachable, 30004 means the number is blocked, 30006 indicates a landline. These failure patterns inform list hygiene — phone numbers that consistently return 30003 across multiple sends should be flagged and eventually removed.
For OTP flows specifically, track time-to-delivery: if the median delivery time for a verification code exceeds 8-10 seconds, users are entering it before it arrives and failing verification. Slow delivery usually indicates a carrier routing issue in specific regions — Twilio's Messaging Insights dashboard shows per-carrier performance data that helps identify which routes are degraded. Set a code expiry of at least 5 minutes (not 60 seconds) to accommodate normal carrier latency variance across geographies.
Building with SMS? Compare Twilio vs Vonage vs Telnyx on APIScout — pricing, global coverage, and developer experience.
Related: Building a Communication Platform, OpenAI Realtime API: Building Voice Applications 2026, Twilio vs Plivo vs Telnyx: SMS & Voice APIs 2026