Skip to main content

How to Integrate Twilio SMS in Any Web App

·APIScout Team
twiliosms apimessagingtutorialapi integration

How to Integrate Twilio SMS in Any Web App

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

Building with SMS? Compare Twilio vs Vonage vs Telnyx on APIScout — pricing, global coverage, and developer experience.

Comments