To secure Twilio integration, you need to: (1) validate webhook signatures using validateRequest() on all incoming Twilio webhooks, (2) implement rate limiting by phone number AND IP to prevent SMS pumping fraud, (3) store credentials (Account SID, Auth Token) in environment variables only, (4) configure geographic permissions in Twilio console to block high-risk regions, and (5) use HTTPS for all webhook URLs. This blueprint prevents costly abuse of your SMS/voice infrastructure.
TL;DR
Twilio webhooks must be validated to prevent spoofing. Use validateRequest() for all incoming webhooks, implement rate limiting to prevent SMS pumping fraud, keep credentials server-side only, and use geographic permissions to limit abuse.
Webhook Validation Twilio
import twilio from 'twilio'
const authToken = process.env.TWILIO_AUTH_TOKEN!
export async function POST(req: Request) {
const url = req.url
const signature = req.headers.get('x-twilio-signature')!
const formData = await req.formData()
const params: Record<string, string> = {}
formData.forEach((value, key) => {
params[key] = value.toString()
})
// Validate the request came from Twilio
const isValid = twilio.validateRequest(authToken, signature, url, params)
if (!isValid) {
console.error('Invalid Twilio webhook signature')
return new Response('Forbidden', { status: 403 })
}
// Process the verified webhook
const from = params.From
const body = params.Body
console.log(`SMS from ${from}: ${body}`)
// Return TwiML response
const twiml = new twilio.twiml.MessagingResponse()
twiml.message('Thanks for your message!')
return new Response(twiml.toString(), {
headers: { 'Content-Type': 'text/xml' },
})
}
Sending SMS Securely
import twilio from 'twilio'
const client = twilio(
process.env.TWILIO_ACCOUNT_SID!,
process.env.TWILIO_AUTH_TOKEN!
)
export async function sendVerificationCode(
phoneNumber: string,
code: string
) {
// Rate limit before sending
const recentAttempts = await getRecentAttempts(phoneNumber)
if (recentAttempts > 3) {
throw new Error('Too many verification attempts')
}
await client.messages.create({
body: `Your verification code is: ${code}`,
from: process.env.TWILIO_PHONE_NUMBER!,
to: phoneNumber,
})
await recordAttempt(phoneNumber)
}
SMS Pumping Prevention
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(3, '1 h'), // 3 per hour
analytics: true,
})
export async function canSendSms(phoneNumber: string, ip: string) {
// Rate limit by phone number
const phoneLimit = await ratelimit.limit(`sms:phone:${phoneNumber}`)
if (!phoneLimit.success) return false
// Rate limit by IP
const ipLimit = await ratelimit.limit(`sms:ip:${ip}`)
if (!ipLimit.success) return false
// Block high-risk country codes (configure based on your needs)
const blockedPrefixes = ['+882', '+883'] // Satellite phones
if (blockedPrefixes.some(p => phoneNumber.startsWith(p))) {
return false
}
return true
}
SMS pumping is expensive. Attackers can trigger thousands of SMS messages to premium-rate numbers. Always rate limit by phone number AND IP, and configure geographic permissions in your Twilio console.
Security Checklist
Pre-Launch Checklist
Webhook signatures validated
Credentials stored in environment variables
Rate limiting implemented
Geographic permissions configured
Webhook URL uses HTTPS
Related Integration Stacks
SendGrid Email Integration Stripe Webhook Patterns Edge Rate Limiting