SMS remains one of the most effective communication channels with 98% open rates. Twilio provides a robust API for sending and receiving SMS messages globally. This comprehensive guide covers everything from basic SMS sending to advanced features like two-factor authentication and bulk messaging.
Getting Started with Twilio
Account Setup
- 1Sign up at twilio.com
- 2Get a phone number from the Console
- 3Note your Account SID and Auth Token
- 4Install the Twilio Node.js helper library
npm install twilioBasic Configuration
// lib/twilio.ts
import twilio from 'twilio';
const accountSid = process.env.TWILIO_ACCOUNT_SID!;
const authToken = process.env.TWILIO_AUTH_TOKEN!;
export const client = twilio(accountSid, authToken);
export const fromNumber = process.env.TWILIO_PHONE_NUMBER!;Sending SMS Messages
Basic SMS Sending
// lib/sms.ts
import { client, fromNumber } from './twilio';
export async function sendSMS(to: string, body: string) {
try {
const message = await client.messages.create({
body,
from: fromNumber,
to,
});
return {
success: true,
messageId: message.sid,
status: message.status,
};
} catch (error) {
console.error('SMS send failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}Send Verification Code
export async function sendVerificationCode(phoneNumber: string): Promise<string> {
// Generate 6-digit code
const code = Math.floor(100000 + Math.random() * 900000).toString();
await sendSMS(
phoneNumber,
`Your verification code is: ${code}. Valid for 10 minutes.`
);
// Store code with expiration
await db.verificationCodes.create({
data: {
phoneNumber,
code,
expiresAt: new Date(Date.now() + 10 * 60 * 1000), // 10 minutes
},
});
return code;
}
export async function verifyCode(phoneNumber: string, code: string): Promise<boolean> {
const record = await db.verificationCodes.findFirst({
where: {
phoneNumber,
code,
expiresAt: {
gte: new Date(),
},
},
});
if (record) {
// Mark as used
await db.verificationCodes.update({
where: { id: record.id },
data: { used: true },
});
return true;
}
return false;
}Two-Factor Authentication (2FA)
export async function enable2FA(userId: string, phoneNumber: string) {
// Verify phone number ownership
const code = await sendVerificationCode(phoneNumber);
// Wait for user to verify
// Then enable 2FA
await db.users.update({
where: { id: userId },
data: {
twoFactorEnabled: true,
twoFactorPhoneNumber: phoneNumber,
},
});
}
export async function verify2FA(userId: string, code: string): Promise<boolean> {
const user = await db.users.findUnique({
where: { id: userId },
});
if (!user.twoFactorEnabled) {
throw new Error('2FA not enabled');
}
return await verifyCode(user.twoFactorPhoneNumber!, code);
}Bulk SMS Messaging
Sending Bulk Messages
interface BulkMessage {
to: string;
body: string;
}
export async function sendBulkSMS(messages: BulkMessage[]) {
const results = [];
// Process in batches to respect rate limits
const batchSize = 100;
for (let i = 0; i < messages.length; i += batchSize) {
const batch = messages.slice(i, i + batchSize);
const promises = batch.map(message => sendSMS(message.to, message.body));
const batchResults = await Promise.all(promises);
results.push(...batchResults);
// Add delay between batches to avoid rate limiting
if (i + batchSize < messages.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return {
total: messages.length,
successful: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length,
results,
};
}Receiving SMS Messages
Webhook Handler
// api/sms/webhook.ts
export async function POST(req: Request) {
const body = await req.formData();
const from = body.get('From') as string;
const to = body.get('To') as string;
const message = body.get('Body') as string;
// Process incoming message
await processIncomingSMS(from, to, message);
// Respond to acknowledge receipt
return new Response('<?xml version="1.0" encoding="UTF-8"?><Response></Response>', {
headers: { 'Content-Type': 'text/xml' },
});
}
async function processIncomingSMS(from: string, to: string, message: string) {
const trimmedMessage = message.trim().toLowerCase();
// Handle different commands
if (trimmedMessage === 'stop' || trimmedMessage === 'unsubscribe') {
await handleUnsubscribe(from);
} else if (trimmedMessage === 'start' || trimmedMessage === 'subscribe') {
await handleSubscribe(from);
} else if (trimmedMessage.startsWith('help')) {
await sendHelp(from);
} else {
// Handle custom logic
await handleCustomCommand(from, message);
}
}
async function handleUnsubscribe(phoneNumber: string) {
await db.subscribers.update({
where: { phoneNumber },
data: { unsubscribed: true },
});
await sendSMS(phoneNumber, 'You have been unsubscribed. Reply START to resubscribe.');
}SMS Templates
Template System
// lib/sms-templates.ts
interface SMSTemplate {
(data: any): string;
}
export const smsTemplates: Record<string, SMSTemplate> = {
welcome: (data) => `Welcome to ${data.appName}! Reply HELP for help, STOP to unsubscribe.`,
otp: (data) => `Your ${data.service} verification code is ${data.code}. Don't share this with anyone.`,
appointmentReminder: (data) => `Reminder: Appointment with ${data.provider} on ${data.date} at ${data.time}. Reply C to cancel.`,
orderUpdate: (data) => `Your order #${data.orderId} is ${data.status}. Track at ${data.trackingUrl}`,
alert: (data) => `Alert: ${data.message}. Reply STOP to opt out.`,
};
export function sendTemplatedSMS(phoneNumber: string, template: string, data: any) {
const templateFn = smsTemplates[template];
if (!templateFn) {
throw new Error(`Template ${template} not found`);
}
const body = templateFn(data);
return sendSMS(phoneNumber, body);
}Advanced Features
Scheduled Messages
export async function scheduleSMS(
to: string,
body: string,
sendAt: Date
) {
// Store in database
const scheduledMessage = await db.scheduledMessages.create({
data: {
to,
body,
sendAt,
},
});
// Schedule job (using your task queue)
await scheduleJob(sendAt, async () => {
await sendSMS(to, body);
await db.scheduledMessages.update({
where: { id: scheduledMessage.id },
data: { sent: true },
});
});
return scheduledMessage.id;
}Message Status Tracking
export async function trackMessageStatus(messageSid: string) {
const message = await client.messages(messageSid).fetch();
return {
sid: message.sid,
status: message.status,
errorCode: message.errorCode,
errorMessage: message.errorMessage,
};
}Best Practices
1. Respect Time Zones
export async function sendSMSSafely(phoneNumber: string, body: string) {
const timeZone = await getTimeZoneFromPhone(phoneNumber);
const localTime = getLocalTime(timeZone);
// Don't send between 9 PM and 8 AM
if (localTime.getHours() >= 21 || localTime.getHours() < 8) {
// Schedule for next morning
const sendAt = getNextMorning(localTime);
return await scheduleSMS(phoneNumber, body, sendAt);
}
return await sendSMS(phoneNumber, body);
}2. Handle Rate Limits
import pLimit from 'p-limit';
const limit = pLimit(10); // Max 10 concurrent requests
export async function sendBulkSMSWithLimit(messages: BulkMessage[]) {
const tasks = messages.map(message =>
limit(() => sendSMS(message.to, message.body))
);
return await Promise.all(tasks);
}3. Implement Opt-Out
Always provide opt-out instructions and respect STOP requests:
export async function includeOptOut(body: string): string {
return `${body}
Reply STOP to opt out, HELP for help.`;
}Testing
Use Test Credentials
// During testing
const testClient = twilio(
process.env.TWILIO_TEST_ACCOUNT_SID!,
process.env.TWILIO_TEST_AUTH_TOKEN!
);
// Test numbers
const testNumbers = {
valid: '+15005550006',
invalid: '+15005550001',
};Conclusion
Twilio provides a powerful, reliable platform for SMS messaging. By implementing:
- Verification systems for secure authentication
- Bulk messaging for notifications
- Webhook handlers for incoming messages
- Template systems for consistency
You'll create effective SMS communication that engages users and drives action.
Remember: SMS is personal. Respect your users by:
- Sending only valuable messages
- Respecting time zones
- Providing easy opt-out
- Keeping messages concise
Used correctly, SMS can significantly improve user engagement and experience.