Claude Code for Twilio: SMS, Voice, and Verify API — Claude Skills 360 Blog
Blog / Backend / Claude Code for Twilio: SMS, Voice, and Verify API
Backend

Claude Code for Twilio: SMS, Voice, and Verify API

Published: May 14, 2027
Read time: 6 min read
By: Claude Skills 360

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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free