Skip to main content

How to Integrate Twilio SMS in 2026

·APIScout Team
Share:

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

  1. Sign up at twilio.com
  2. Get a phone number (free with trial)
  3. 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 TypeUS/CanadaInternational
SMS (outbound)$0.0079$0.05-$0.15
SMS (inbound)$0.0075Varies
MMS (outbound)$0.0200Not available
Phone number$1.15/monthVaries
Verify OTP$0.05/verification$0.05+

Trial account: $15 free credit. Can only send to verified numbers.

Common Mistakes

MistakeImpactFix
Not using E.164 formatMessages fail to sendAlways use +[country][number] format
Ignoring STOP/unsubscribeLegal violations (TCPA)Honor opt-outs automatically
No rate limitingTwilio rate limits you, messages queueLimit to 1 msg/sec per number
Not handling delivery failuresSilent message lossUse status callbacks
Sending from trial to unverified numbersMessages failUpgrade or verify recipient numbers
Exposing Auth TokenAccount compromiseServer-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

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.