Twilio is a cloud communications platform — twilio.messages.create({ to, from, body }) sends SMS. twilio.calls.create({ to, from, url }) initiates voice calls with a TwiML URL. The Verify API handles OTP: twilio.verify.v2.services(sid).verifications.create({ to: phone, channel: "sms" }) sends a code; verificationChecks.create({ to: phone, code }) confirms it. Webhook validation uses twilio.validateRequest(authToken, sig, url, params). twilio.messages.list({ to, dateSentAfter }) queries message history. Messaging Services send from a pool of numbers for high-volume SMS. Conversations API manages two-way long-form messaging threads. twilio.lookups.v2.phoneNumbers(number).fetch() validates and formats E.164 phone numbers. WhatsApp messages use to: "whatsapp:+1...". Claude Code generates Twilio OTP flows, SMS notification systems, voice IVR, phone verification middleware, and status callbacks.
CLAUDE.md for Twilio
## Twilio Stack
- Version: twilio >= 5.3
- Init: const client = twilio(process.env.TWILIO_ACCOUNT_SID!, process.env.TWILIO_AUTH_TOKEN!)
- SMS: await client.messages.create({ to: "+15551234567", from: process.env.TWILIO_PHONE!, body: "Your code: 123456" })
- Verify: await client.verify.v2.services(VERIFY_SID).verifications.create({ to: phone, channel: "sms" })
- Check: const check = await client.verify.v2.services(VERIFY_SID).verificationChecks.create({ to: phone, code })
- Validate: twilio.validateRequest(authToken, sig, url, params) — webhook signature check
- Lookup: await client.lookups.v2.phoneNumbers(phone).fetch({ fields: "line_type_intelligence" })
SMS and Verify Utilities
// lib/twilio/client.ts — Twilio client and utilities
import twilio from "twilio"
import { z } from "zod"
const client = twilio(
process.env.TWILIO_ACCOUNT_SID!,
process.env.TWILIO_AUTH_TOKEN!,
)
const VERIFY_SID = process.env.TWILIO_VERIFY_SID!
const FROM_NUMBER = process.env.TWILIO_PHONE_NUMBER!
// ── Send SMS ───────────────────────────────────────────────────────────────
export async function sendSMS(
to: string,
body: string,
options: { messagingServiceSid?: string; statusCallbackUrl?: string } = {},
): Promise<string> {
const msg = await client.messages.create({
to,
body,
...(options.messagingServiceSid
? { messagingServiceSid: options.messagingServiceSid }
: { from: FROM_NUMBER }),
...(options.statusCallbackUrl && {
statusCallback: options.statusCallbackUrl,
}),
})
return msg.sid
}
// ── OTP via Verify API ─────────────────────────────────────────────────────
export type VerifyChannel = "sms" | "whatsapp" | "email" | "call"
export async function sendVerificationCode(
to: string,
channel: VerifyChannel = "sms",
): Promise<void> {
await client.verify.v2
.services(VERIFY_SID)
.verifications.create({ to, channel })
}
export async function checkVerificationCode(
to: string,
code: string,
): Promise<{ valid: boolean; status: string }> {
const check = await client.verify.v2
.services(VERIFY_SID)
.verificationChecks.create({ to, code })
return {
valid: check.status === "approved",
status: check.status,
}
}
// ── Phone number validation ────────────────────────────────────────────────
export async function validatePhoneNumber(
phone: string,
): Promise<{ e164: string; isValid: boolean; lineType: string | null }> {
try {
const lookup = await client.lookups.v2
.phoneNumbers(phone)
.fetch({ fields: "line_type_intelligence" })
return {
e164: lookup.phoneNumber ?? phone,
isValid: lookup.valid ?? false,
lineType: (lookup.lineTypeIntelligence as any)?.type ?? null,
}
} catch {
return { e164: phone, isValid: false, lineType: null }
}
}
// ── Webhook signature validation ───────────────────────────────────────────
export function validateTwilioWebhook(
url: string,
signature: string,
params: Record<string, string>,
): boolean {
return twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN!,
signature,
url,
params,
)
}
Phone Verification API Routes (Next.js)
// app/api/auth/phone/send/route.ts — send OTP
import { NextRequest, NextResponse } from "next/server"
import { sendVerificationCode, validatePhoneNumber } from "@/lib/twilio/client"
import { rateLimit } from "@/lib/rate-limit"
import { z } from "zod"
const sendSchema = z.object({
phone: z.string().min(7).max(20),
channel: z.enum(["sms", "whatsapp"]).default("sms"),
})
export async function POST(request: NextRequest) {
// Rate limit: 3 attempts per phone per 10 minutes
const ip = request.headers.get("x-forwarded-for") ?? "127.0.0.1"
const limited = await rateLimit(`phone-otp:${ip}`, 3, 600)
if (!limited.success) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 })
}
const body = sendSchema.safeParse(await request.json())
if (!body.success) {
return NextResponse.json({ error: "Invalid phone number" }, { status: 400 })
}
const { phone, channel } = body.data
// Validate and normalize phone
const { e164, isValid, lineType } = await validatePhoneNumber(phone)
if (!isValid) {
return NextResponse.json({ error: "Invalid phone number" }, { status: 422 })
}
// Block VOIP numbers for SMS OTP (optional security measure)
if (lineType === "voip" && channel === "sms") {
return NextResponse.json({ error: "VOIP numbers are not supported" }, { status: 422 })
}
await sendVerificationCode(e164, channel)
return NextResponse.json({ ok: true, phone: e164 })
}
// app/api/auth/phone/verify/route.ts — verify OTP
import { NextRequest, NextResponse } from "next/server"
import { checkVerificationCode } from "@/lib/twilio/client"
import { lucia } from "@/lib/auth/lucia"
import { db } from "@/lib/db"
import { users } from "@/lib/db/schema"
import { eq } from "drizzle-orm"
import { generateId } from "lucia"
import { cookies } from "next/headers"
import { z } from "zod"
const verifySchema = z.object({
phone: z.string(),
code: z.string().length(6).regex(/^\d+$/),
})
export async function POST(request: NextRequest) {
const body = verifySchema.safeParse(await request.json())
if (!body.success) {
return NextResponse.json({ error: "Invalid input" }, { status: 400 })
}
const { phone, code } = body.data
const { valid } = await checkVerificationCode(phone, code)
if (!valid) {
return NextResponse.json({ error: "Invalid or expired code" }, { status: 422 })
}
// Upsert user by phone
let user = await db.query.users.findFirst({
where: eq(users.phone, phone),
})
if (!user) {
const userId = generateId(15)
await db.insert(users).values({
id: userId,
phone,
phoneVerified: true,
role: "user",
name: "New User",
email: `phone-${userId}@placeholder.com`,
emailVerified: false,
})
user = await db.query.users.findFirst({ where: eq(users.id, userId) })
} else {
await db.update(users).set({ phoneVerified: true }).where(eq(users.phone, phone))
}
// Create session
const session = await lucia.createSession(user!.id, {})
const cookie = lucia.createSessionCookie(session.id)
const cookieStore = await cookies()
cookieStore.set(cookie.name, cookie.value, cookie.attributes)
return NextResponse.json({ ok: true, userId: user!.id })
}
Notification Service
// lib/notifications/sms.ts — SMS notification templates
import { sendSMS } from "@/lib/twilio/client"
import { db } from "@/lib/db"
import { eq } from "drizzle-orm"
import { users } from "@/lib/db/schema"
type SmsNotificationType =
| { type: "order_confirmed"; orderId: string; total: string }
| { type: "password_reset"; resetUrl: string }
| { type: "appointment_reminder"; time: string; location: string }
| { type: "shipping_update"; trackingNumber: string; status: string }
const TEMPLATES: Record<SmsNotificationType["type"], (data: any) => string> = {
order_confirmed: ({ orderId, total }) =>
`Your order #${orderId} is confirmed! Total: ${total}. Track it at yourdomain.com/orders/${orderId}`,
password_reset: ({ resetUrl }) =>
`Reset your password: ${resetUrl} (expires in 1 hour). If you didn't request this, ignore this message.`,
appointment_reminder: ({ time, location }) =>
`Reminder: You have an appointment at ${time} at ${location}. Reply STOP to opt out.`,
shipping_update: ({ trackingNumber, status }) =>
`Your package (${trackingNumber}) status: ${status}. Track at yourdomain.com/track/${trackingNumber}`,
}
export async function sendNotification(
userId: string,
notification: SmsNotificationType,
): Promise<void> {
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
columns: { phone: true, smsOptIn: true },
})
if (!user?.phone || !user.smsOptIn) return
const body = TEMPLATES[notification.type](notification)
await sendSMS(user.phone, body)
}
Webhook Handler
// app/api/webhooks/twilio/route.ts — incoming SMS and status callbacks
import { NextRequest, NextResponse } from "next/server"
import { validateTwilioWebhook } from "@/lib/twilio/client"
import { db } from "@/lib/db"
export async function POST(request: NextRequest) {
const formData = await request.formData()
const params = Object.fromEntries(formData.entries()) as Record<string, string>
const signature = request.headers.get("x-twilio-signature") ?? ""
const url = request.url
if (!validateTwilioWebhook(url, signature, params)) {
return new Response("Invalid signature", { status: 403 })
}
const eventType = params["EventType"] ?? "incoming_message"
if (eventType === "incoming_message" || params["MessageStatus"] === undefined) {
// Incoming SMS reply
const from = params["From"]
const body = params["Body"]?.trim()
if (body?.toUpperCase() === "STOP") {
// Handle opt-out
await db.update(users).set({ smsOptIn: false }).where(eq(users.phone, from))
} else {
// Handle other replies...
}
} else {
// Status callback — update message status in DB
const sid = params["MessageSid"]
const status = params["MessageStatus"]
await db.update(smsMessages).set({ status }).where(eq(smsMessages.twilioSid, sid))
}
// Twilio expects 200 with empty TwiML
return new Response("<Response/>", {
headers: { "Content-Type": "text/xml" },
})
}
For the AWS SNS alternative when sending SMS at scale with per-message pricing, no monthly service fee, and tight AWS IAM integration for existing AWS-native infrastructure is preferred — SNS has cheaper bulk SMS rates than Twilio but requires managing E.164 formatting, opt-out lists, and 10DLC registration manually, see the AWS SNS guide. For the Vonage (Nexmo) alternative when a comparable SMS API with competitive pricing in European and Asian markets is needed — Vonage and Twilio have near-identical APIs for SMS and voice, with Vonage often having better pricing outside North America. The Claude Skills 360 bundle includes Twilio skill sets covering SMS, OTP verification, and webhook handling. Start with the free tier to try communications API generation.