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

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

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

Vonage (formerly Nexmo) is a communications API platform — new Vonage({ apiKey, apiSecret }) creates the SDK. vonage.sms.send({ to, from, text }) sends an SMS. vonage.verify.start({ number, brand, codeLength }) begins OTP verification; vonage.verify.check({ requestId, code }) validates the code. vonage.voice.createOutboundCall({ to, from, ncco }) makes a voice call with NCCO (Nexmo Call Control Objects). NCCO actions include talk for text-to-speech, record for call recording, and connect for call forwarding. vonage.messages.send({ channel, to, from, text }) uses the Messages API for WhatsApp and MMS. Webhook signature verified with Vonage.verifySignature(params, secret). Inbound SMS hits the webhook URL configured in Vonage dashboard — req.body.text and req.body.msisdn contain the message and sender. Claude Code generates Vonage SMS delivery, OTP flows, voice call NCCO, and webhook handling.

CLAUDE.md for Vonage

## Vonage Stack
- Version: @vonage/server-sdk >= 3.12
- Init: const vonage = new Vonage({ apiKey: VONAGE_API_KEY, apiSecret: VONAGE_API_SECRET })
- SMS: await vonage.sms.send({ to: "15551234567", from: "Acme", text: "Your code is 123456" })
- Verify start: const { requestId } = await vonage.verify.start({ number, brand: "Acme", codeLength: 6 })
- Verify check: const { status } = await vonage.verify.check({ requestId, code })
- Voice: await vonage.voice.createOutboundCall({ to: [{ type: "phone", number }], from: { type: "phone", number: FROM_NUMBER }, ncco: [{ action: "talk", text }] })
- Webhook verify: Vonage.verifySignature(req.query, VONAGE_SIGNATURE_SECRET)

Vonage Server Utilities

// lib/communications/vonage.ts — Vonage SDK utilities
import { Vonage } from "@vonage/server-sdk"
import { Auth } from "@vonage/auth"
import type { SMSParams } from "@vonage/sms"

const auth = new Auth({
  apiKey: process.env.VONAGE_API_KEY!,
  apiSecret: process.env.VONAGE_API_SECRET!,
})

export const vonage = new Vonage(auth)

const FROM_NUMBER = process.env.VONAGE_FROM_NUMBER!  // Your Vonage virtual number
const BRAND_NAME = process.env.APP_NAME ?? "MyApp"

// ── SMS ────────────────────────────────────────────────────────────────────

export type SMSResult = {
  success: boolean
  messageId?: string
  errorText?: string
}

export async function sendSMS(to: string, text: string): Promise<SMSResult> {
  try {
    const response = await vonage.sms.send({
      to: to.replace(/\D/g, ""),  // Strip non-digits
      from: FROM_NUMBER,
      text,
    })

    const message = response.messages[0]

    if (message.status !== "0") {
      return { success: false, errorText: message["error-text"] }
    }

    return { success: true, messageId: message["message-id"] }
  } catch (err) {
    console.error("[Vonage SMS]", err)
    return { success: false, errorText: String(err) }
  }
}

// Common SMS patterns
export async function sendOrderConfirmationSMS(phone: string, params: {
  orderId: string
  total: string
  estimatedDelivery: string
}): Promise<void> {
  await sendSMS(phone,
    `Order #${params.orderId} confirmed! Total: ${params.total}. Est. delivery: ${params.estimatedDelivery}. Reply STOP to opt out.`,
  )
}

export async function sendShippingUpdateSMS(phone: string, params: {
  orderId: string
  trackingUrl: string
}): Promise<void> {
  await sendSMS(phone,
    `Your order #${params.orderId} has shipped! Track: ${params.trackingUrl}`,
  )
}

// ── OTP / Verify ──────────────────────────────────────────────────────────

type VerifyStatus = "success" | "invalid_code" | "expired" | "already_verified" | "error"

export async function startPhoneVerification(phoneNumber: string): Promise<{
  requestId: string | null
  error?: string
}> {
  try {
    const response = await vonage.verify.start({
      number: phoneNumber.replace(/\D/g, ""),
      brand: BRAND_NAME,
      codeLength: 6,
      lg: "en-us",
      nextEventWait: 120,  // 2 min between channels
    })

    if (response.status !== "0") {
      return { requestId: null, error: response.errorText }
    }

    return { requestId: response.requestId }
  } catch (err) {
    return { requestId: null, error: String(err) }
  }
}

export async function checkVerificationCode(
  requestId: string,
  code: string,
): Promise<{ status: VerifyStatus }> {
  try {
    const response = await vonage.verify.check({ requestId, code })

    const statusMap: Record<string, VerifyStatus> = {
      "0": "success",
      "16": "invalid_code",
      "6": "expired",
      "17": "already_verified",
    }

    return { status: statusMap[response.status] ?? "error" }
  } catch (err) {
    console.error("[Vonage verify check]", err)
    return { status: "error" }
  }
}

export async function cancelVerification(requestId: string): Promise<void> {
  await vonage.verify.cancel(requestId)
}

// ── Voice ─────────────────────────────────────────────────────────────────

export type NCCOAction =
  | { action: "talk"; text: string; bargeIn?: boolean; voiceName?: string }
  | { action: "record"; eventUrl: string[]; endOnSilence?: number }
  | { action: "connect"; endpoint: [{ type: "phone"; number: string }]; timeout?: number }
  | { action: "input"; type: ["dtmf"]; dtmf: { digits?: string; submitOnHash?: boolean; timeOut?: number } }

export async function makeOutboundCall(to: string, ncco: NCCOAction[]): Promise<{
  callUuid: string | null
  error?: string
}> {
  try {
    const response = await vonage.voice.createOutboundCall({
      to: [{ type: "phone", number: to.replace(/\D/g, "") }],
      from: { type: "phone", number: FROM_NUMBER },
      ncco,
    })

    return { callUuid: response.uuid ?? null }
  } catch (err) {
    console.error("[Vonage voice]", err)
    return { callUuid: null, error: String(err) }
  }
}

// Text-to-speech call
export async function sayMessage(to: string, message: string): Promise<void> {
  await makeOutboundCall(to, [
    { action: "talk", text: message, voiceName: "Amy" },
  ])
}

Next.js API Routes

// app/api/verify/phone/route.ts — start OTP
import { NextRequest, NextResponse } from "next/server"
import { auth } from "@clerk/nextjs/server"
import { startPhoneVerification } from "@/lib/communications/vonage"
import { rateLimiter } from "@/lib/rate-limit"
import { z } from "zod"

const PhoneSchema = z.object({
  phone: z.string().regex(/^\+?[1-9]\d{7,14}$/, "Invalid phone number"),
})

export async function POST(request: NextRequest) {
  const { userId } = await auth()
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })

  // Rate limit: 3 verify starts per hour per user
  const limited = await rateLimiter.check(`verify:${userId}`, 3, "1h")
  if (limited) {
    return NextResponse.json({ error: "Too many verification attempts" }, { status: 429 })
  }

  const body = PhoneSchema.safeParse(await request.json())
  if (!body.success) {
    return NextResponse.json({ error: body.error.issues[0].message }, { status: 400 })
  }

  const { requestId, error } = await startPhoneVerification(body.data.phone)

  if (!requestId) {
    return NextResponse.json({ error: error ?? "Verification failed" }, { status: 400 })
  }

  // Store requestId in session (not exposed to client)
  // In production, store in Redis with userId → requestId mapping
  return NextResponse.json({ success: true })
}
// app/api/verify/phone/check/route.ts — validate OTP code
import { NextRequest, NextResponse } from "next/server"
import { auth } from "@clerk/nextjs/server"
import { checkVerificationCode } from "@/lib/communications/vonage"
import { db } from "@/lib/db"
import { users } from "@/lib/db/schema"
import { eq } from "drizzle-orm"

export async function POST(request: NextRequest) {
  const { userId } = await auth()
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })

  const { requestId, code } = await request.json()

  const { status } = await checkVerificationCode(requestId, code)

  if (status !== "success") {
    const messages = {
      invalid_code: "Incorrect code. Please try again.",
      expired: "Code has expired. Please request a new one.",
      already_verified: "This number has already been verified.",
      error: "Verification failed. Please try again.",
    }
    return NextResponse.json(
      { error: messages[status as keyof typeof messages] ?? "Verification failed" },
      { status: 422 },
    )
  }

  // Mark phone as verified in DB (phone stored from verify start in real app)
  await db.update(users)
    .set({ phoneVerified: true })
    .where(eq(users.id, userId))

  return NextResponse.json({ success: true })
}

Inbound SMS Webhook

// app/api/webhooks/vonage/sms/route.ts — handle inbound SMS
import { NextRequest, NextResponse } from "next/server"
import { Vonage } from "@vonage/server-sdk"

export async function POST(request: NextRequest) {
  const body = await request.json()

  // Verify webhook signature
  const isValid = Vonage.verifySignature(
    { ...body, ...Object.fromEntries(request.nextUrl.searchParams) },
    process.env.VONAGE_SIGNATURE_SECRET!,
  )

  if (!isValid) {
    return new Response("Invalid signature", { status: 401 })
  }

  const { msisdn: from, to, text, messageId } = body

  console.log(`[Vonage inbound SMS] from=${from} to=${to} text="${text}" id=${messageId}`)

  // Handle opt-out
  if (text?.trim().toUpperCase() === "STOP") {
    await handleSMSOptOut(from)
  }

  // Handle HELP
  if (text?.trim().toUpperCase() === "HELP") {
    const { sendSMS } = await import("@/lib/communications/vonage")
    await sendSMS(from, `Reply STOP to unsubscribe. For help visit ${process.env.APP_URL}/help`)
  }

  return new Response("OK", { status: 200 })
}

// GET handler for Vonage delivery receipts
export async function GET(request: NextRequest) {
  const params = Object.fromEntries(request.nextUrl.searchParams)

  console.log("[Vonage DLR]", {
    messageId: params["message-id"],
    status: params.status,
    msisdn: params.msisdn,
  })

  return new Response("OK", { status: 200 })
}

async function handleSMSOptOut(phoneNumber: string): Promise<void> {
  const { db } = await import("@/lib/db")
  const { users } = await import("@/lib/db/schema")
  const { eq } = await import("drizzle-orm")

  await db.update(users)
    .set({ smsOptOut: true })
    .where(eq(users.phone, phoneNumber))
}

For the Twilio alternative when a more mature, enterprise-grade communications platform with a larger global SMS carrier network, better delivery rates, advanced phone number provisioning, and a richer Conversations API for WhatsApp is needed — Twilio has broader global coverage and more carrier partnerships while Vonage has competitive pricing and unified API surface, see the Twilio guide. For the Plivo alternative when low-cost SMS delivery with competitive international rates and an AWS-style console is preferred — Plivo focuses on cost-effective bulk SMS while Vonage has richer voice and OTP verification features, see the Plivo guide. The Claude Skills 360 bundle includes Vonage skill sets covering SMS, OTP verification, and voice calls. 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