Skip to content
All essays
CraftJanuary 15, 202610 min

Complete Guide to SMS Integration with Twilio

Master SMS messaging with Twilio API including verification codes, notifications, two-factor authentication, and bulk messaging

Ü
Ümit Uz
Mobile & Full Stack Developer

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

  1. 1Sign up at twilio.com
  2. 2Get a phone number from the Console
  3. 3Note your Account SID and Auth Token
  4. 4Install the Twilio Node.js helper library
bash
npm install twilio

Basic Configuration

typescript
// 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

typescript
// 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

typescript
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)

typescript
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

typescript
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

typescript
// 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

typescript
// 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

typescript
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

typescript
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

typescript
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

typescript
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:

typescript
export async function includeOptOut(body: string): string {
  return `${body}

Reply STOP to opt out, HELP for help.`;
}

Testing

Use Test Credentials

typescript
// 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.

Enjoyed this essay? Share it.

Related essays

Next essay
SMS Marketing Strategies That Drive Engagement