git clone https://github.com/vibeforge1111/vibeship-spawner-skills
integrations/twilio-communications/skill.yamlTwilio Communications Skill
SMS, Voice, WhatsApp, and Verify API integration patterns
id: twilio-communications name: Twilio Communications category: integrations description: | Build communication features with Twilio: SMS messaging, voice calls, WhatsApp Business API, and user verification (2FA). Covers the full spectrum from simple notifications to complex IVR systems and multi-channel authentication. Critical focus on compliance, rate limits, and error handling.
version: 1.0.0
triggers:
- "twilio"
- "send SMS"
- "text message"
- "voice call"
- "phone verification"
- "2FA SMS"
- "WhatsApp API"
- "programmable messaging"
- "IVR system"
- "TwiML"
- "phone number verification"
============================================================================
CORE PATTERNS
============================================================================
patterns:
-
id: sms-sending-pattern name: SMS Sending Pattern description: | Basic pattern for sending SMS messages with Twilio. Handles the fundamentals: phone number formatting, message delivery, and delivery status callbacks.
Key considerations:
- Phone numbers must be in E.164 format (+1234567890)
- Default rate limit: 80 messages per second (MPS)
- Messages over 160 characters are split (and cost more)
- Carrier filtering can block messages (especially to US numbers) when_to_use:
- "Sending notifications to users"
- "Transactional messages (order confirmations, shipping)"
- "Alerts and reminders" implementation: | from twilio.rest import Client from twilio.base.exceptions import TwilioRestException import os import re
class TwilioSMS: """ SMS sending with proper error handling and validation. """
def __init__(self): self.client = Client( os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"] ) self.from_number = os.environ["TWILIO_PHONE_NUMBER"] def validate_e164(self, phone: str) -> bool: """Validate phone number is in E.164 format.""" pattern = r'^\+[1-9]\d{1,14}$' return bool(re.match(pattern, phone)) def send_sms( self, to: str, body: str, status_callback: str = None ) -> dict: """ Send an SMS message. Args: to: Recipient phone number in E.164 format body: Message text (160 chars = 1 segment) status_callback: URL for delivery status webhooks Returns: Message SID and status """ # Validate phone number format if not self.validate_e164(to): return { "success": False, "error": "Phone number must be in E.164 format (+1234567890)" } # Check message length (warn about segmentation) segment_count = (len(body) + 159) // 160 if segment_count > 1: print(f"Warning: Message will be sent as {segment_count} segments") try: message = self.client.messages.create( to=to, from_=self.from_number, body=body, status_callback=status_callback ) return { "success": True, "message_sid": message.sid, "status": message.status, "segments": segment_count } except TwilioRestException as e: return self._handle_error(e) def _handle_error(self, error: TwilioRestException) -> dict: """Handle Twilio-specific errors.""" error_handlers = { 21610: "Recipient has opted out. They must reply START.", 21614: "Invalid 'To' phone number format.", 21211: "'From' phone number is not valid.", 30003: "Phone is unreachable (off, airplane mode, no signal).", 30005: "Unknown destination (invalid number or landline).", 30006: "Landline or unreachable carrier.", 30429: "Rate limit exceeded. Implement exponential backoff.", } return { "success": False, "error_code": error.code, "error": error_handlers.get(error.code, error.msg), "details": str(error) }Usage
sms = TwilioSMS() result = sms.send_sms( to="+14155551234", body="Your order #1234 has shipped!", status_callback="https://your-app.com/webhooks/twilio/status" ) anti_patterns:
- "Not validating E.164 format before sending"
- "Hardcoding Twilio credentials in code"
- "Ignoring delivery status callbacks"
- "Not handling the opted-out (21610) error"
-
id: twilio-verify-pattern name: Twilio Verify Pattern (2FA/OTP) description: | Use Twilio Verify for phone number verification and 2FA. Handles code generation, delivery, rate limiting, and fraud prevention.
Key benefits over DIY OTP:
- Twilio manages code generation and expiration
- Built-in fraud prevention (saved customers $82M+ blocking 747M attempts)
- Handles rate limiting automatically
- Multi-channel: SMS, Voice, Email, Push, WhatsApp
Google found SMS 2FA blocks "100% of automated bots, 96% of bulk phishing attacks, and 76% of targeted attacks." when_to_use:
- "User phone number verification at signup"
- "Two-factor authentication (2FA)"
- "Password reset verification"
- "High-value transaction confirmation" implementation: | from twilio.rest import Client from twilio.base.exceptions import TwilioRestException import os from enum import Enum from typing import Optional
class VerifyChannel(Enum): SMS = "sms" CALL = "call" EMAIL = "email" WHATSAPP = "whatsapp"
class TwilioVerify: """ Phone verification with Twilio Verify. Never store OTP codes - Twilio handles it. """
def __init__(self, verify_service_sid: str = None): self.client = Client( os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"] ) # Create a Verify Service in Twilio Console first self.service_sid = verify_service_sid or os.environ["TWILIO_VERIFY_SID"] def send_verification( self, to: str, channel: VerifyChannel = VerifyChannel.SMS, locale: str = "en" ) -> dict: """ Send verification code to phone/email. Args: to: Phone number (E.164) or email channel: SMS, call, email, or whatsapp locale: Language code for message Returns: Verification status """ try: verification = self.client.verify \ .v2 \ .services(self.service_sid) \ .verifications \ .create( to=to, channel=channel.value, locale=locale ) return { "success": True, "status": verification.status, # "pending" "channel": channel.value, "valid": verification.valid } except TwilioRestException as e: return self._handle_verify_error(e) def check_verification(self, to: str, code: str) -> dict: """ Check if verification code is correct. Args: to: Phone number or email that received code code: The code entered by user Returns: Verification result """ try: check = self.client.verify \ .v2 \ .services(self.service_sid) \ .verification_checks \ .create( to=to, code=code ) return { "success": True, "valid": check.status == "approved", "status": check.status # "approved" or "pending" } except TwilioRestException as e: # Code was wrong or expired return { "success": False, "valid": False, "error": str(e) } def _handle_verify_error(self, error: TwilioRestException) -> dict: """Handle Verify-specific errors.""" error_handlers = { 60200: "Invalid phone number format", 60203: "Max send attempts reached for this number", 60205: "Service not found - check VERIFY_SID", 60223: "Failed to create verification - carrier rejected", } return { "success": False, "error_code": error.code, "error": error_handlers.get(error.code, error.msg) }Usage Example - Signup Flow
verify = TwilioVerify()
Step 1: User enters phone number
result = verify.send_verification("+14155551234", VerifyChannel.SMS) if result["success"]: print("Code sent! Check your phone.")
Step 2: User enters the code they received
code = "123456" # From user input check = verify.check_verification("+14155551234", code)
if check["valid"]: print("Phone verified! Create account.") else: print("Invalid code. Try again.")
Best Practice: Offer voice fallback
async def verify_with_fallback(phone: str, max_attempts: int = 3): """Verify with voice fallback if SMS fails.""" for attempt in range(max_attempts): channel = VerifyChannel.SMS if attempt == 0 else VerifyChannel.CALL result = verify.send_verification(phone, channel)
if result["success"]: return result # If SMS failed, wait and try voice if channel == VerifyChannel.SMS: await asyncio.sleep(30) continue return {"success": False, "error": "All verification attempts failed"}anti_patterns:
- "Storing OTP codes in your database (Twilio handles this)"
- "Not implementing rate limiting on your verify endpoint"
- "Using same-code retries (let Verify generate new codes)"
- "No fallback channel when SMS fails"
-
id: twiml-ivr-pattern name: TwiML IVR Pattern description: | Build Interactive Voice Response (IVR) systems using TwiML. TwiML (Twilio Markup Language) is XML that tells Twilio what to do when receiving calls.
Core TwiML verbs:
- <Say>: Text-to-speech
- <Play>: Play audio file
- <Gather>: Collect keypad/speech input
- <Dial>: Connect to another number
- <Record>: Record caller's voice
- <Redirect>: Move to another TwiML endpoint
Key insight: Twilio makes HTTP request to your webhook, you return TwiML, Twilio executes it. Stateless, so use URL params or sessions. when_to_use:
- "Phone menu systems (press 1 for sales...)"
- "Automated customer support"
- "Appointment reminders with confirmation"
- "Voicemail systems" implementation: | from flask import Flask, request, Response from twilio.twiml.voice_response import VoiceResponse, Gather from twilio.request_validator import RequestValidator import os
app = Flask(name)
def validate_twilio_request(f): """Decorator to validate requests are from Twilio.""" def wrapper(*args, **kwargs): validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
# Get request details url = request.url params = request.form.to_dict() signature = request.headers.get("X-Twilio-Signature", "") if not validator.validate(url, params, signature): return "Invalid request", 403 return f(*args, **kwargs) wrapper.__name__ = f.__name__ return wrapper@app.route("/voice/incoming", methods=["POST"]) @validate_twilio_request def incoming_call(): """Handle incoming call with IVR menu.""" response = VoiceResponse()
# Gather digits with timeout gather = Gather( num_digits=1, action="/voice/menu-selection", method="POST", timeout=5 ) gather.say( "Welcome to Acme Corp. " "Press 1 for sales. " "Press 2 for support. " "Press 3 to leave a message." ) response.append(gather) # If no input, repeat response.redirect("/voice/incoming") return Response(str(response), mimetype="text/xml")@app.route("/voice/menu-selection", methods=["POST"]) @validate_twilio_request def menu_selection(): """Route based on menu selection.""" response = VoiceResponse() digit = request.form.get("Digits", "")
if digit == "1": # Transfer to sales response.say("Connecting you to sales.") response.dial(os.environ["SALES_PHONE"]) elif digit == "2": # Transfer to support response.say("Connecting you to support.") response.dial(os.environ["SUPPORT_PHONE"]) elif digit == "3": # Voicemail response.say("Please leave a message after the beep.") response.record( action="/voice/voicemail-saved", max_length=120, transcribe=True, transcribe_callback="/voice/transcription" ) else: response.say("Invalid selection.") response.redirect("/voice/incoming") return Response(str(response), mimetype="text/xml")@app.route("/voice/voicemail-saved", methods=["POST"]) @validate_twilio_request def voicemail_saved(): """Handle saved voicemail.""" response = VoiceResponse()
recording_url = request.form.get("RecordingUrl") recording_sid = request.form.get("RecordingSid") # Save to database, notify team, etc. print(f"Voicemail saved: {recording_url}") response.say("Thank you. Goodbye.") response.hangup() return Response(str(response), mimetype="text/xml")@app.route("/voice/transcription", methods=["POST"]) @validate_twilio_request def transcription_callback(): """Handle voicemail transcription.""" transcription = request.form.get("TranscriptionText") recording_sid = request.form.get("RecordingSid")
# Save transcription, send to Slack, etc. print(f"Transcription: {transcription}") return "", 200Outbound call example
from twilio.rest import Client
def make_outbound_call(to: str, message: str): """Make outbound call with custom TwiML.""" client = Client( os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"] )
# TwiML Bin URL or your endpoint call = client.calls.create( to=to, from_=os.environ["TWILIO_PHONE_NUMBER"], url="https://your-app.com/voice/outbound-message", status_callback="https://your-app.com/voice/status" ) return call.sidif name == "main": app.run(debug=True) anti_patterns:
- "Not validating X-Twilio-Signature (security risk)"
- "Returning non-XML responses to Twilio"
- "Not handling timeout/no-input cases"
- "Hardcoding phone numbers in TwiML"
-
id: whatsapp-business-pattern name: WhatsApp Business API Pattern description: | Send and receive WhatsApp messages via Twilio API. Uses the same Twilio Messages API as SMS with minor changes.
Key WhatsApp rules:
- 24-hour session window: Can only reply within 24 hours of user message
- Template messages: Pre-approved templates for outside session window
- Opt-in required: Users must explicitly consent to receive messages
- Rate limit: 80 MPS default (up to 400 with approval)
- Character limits: Non-template 1024 chars, templates ~550 chars when_to_use:
- "Customer support with rich media"
- "Order notifications with buttons"
- "Marketing messages (with templates)"
- "Interactive flows (booking, surveys)" implementation: | from twilio.rest import Client from twilio.base.exceptions import TwilioRestException import os from datetime import datetime, timedelta from typing import Optional
class TwilioWhatsApp: """ WhatsApp Business API via Twilio. Handles session windows and template messages. """
def __init__(self): self.client = Client( os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"] ) # WhatsApp number format: whatsapp:+14155551234 self.from_number = os.environ["TWILIO_WHATSAPP_NUMBER"] def send_message( self, to: str, body: str, media_url: Optional[str] = None ) -> dict: """ Send WhatsApp message within 24-hour session. Args: to: Recipient number (E.164, without whatsapp: prefix) body: Message text (max 1024 chars for non-template) media_url: Optional image/document URL Returns: Message result """ # Format for WhatsApp to_whatsapp = f"whatsapp:{to}" from_whatsapp = f"whatsapp:{self.from_number}" try: message_params = { "to": to_whatsapp, "from_": from_whatsapp, "body": body } if media_url: message_params["media_url"] = [media_url] message = self.client.messages.create(**message_params) return { "success": True, "message_sid": message.sid, "status": message.status } except TwilioRestException as e: return self._handle_whatsapp_error(e) def send_template_message( self, to: str, content_sid: str, content_variables: dict ) -> dict: """ Send pre-approved template message. Use this for messages outside 24-hour window. Content templates must be approved by WhatsApp first. Create them in Twilio Console > Content Template Builder. """ to_whatsapp = f"whatsapp:{to}" from_whatsapp = f"whatsapp:{self.from_number}" try: message = self.client.messages.create( to=to_whatsapp, from_=from_whatsapp, content_sid=content_sid, content_variables=content_variables ) return { "success": True, "message_sid": message.sid, "template": True } except TwilioRestException as e: return self._handle_whatsapp_error(e) def _handle_whatsapp_error(self, error: TwilioRestException) -> dict: """Handle WhatsApp-specific errors.""" error_handlers = { 63016: "Outside 24-hour window. Use template message.", 63018: "Template not approved or doesn't exist.", 63025: "Too many template messages sent to this user.", 63038: "Rate limit exceeded for WhatsApp.", } return { "success": False, "error_code": error.code, "error": error_handlers.get(error.code, error.msg) }Flask webhook for incoming WhatsApp messages
from flask import Flask, request
app = Flask(name)
@app.route("/webhooks/whatsapp", methods=["POST"]) def whatsapp_webhook(): """Handle incoming WhatsApp messages.""" from_number = request.form.get("From", "").replace("whatsapp:", "") body = request.form.get("Body", "") media_url = request.form.get("MediaUrl0") # First attachment
# Track session start (24-hour window begins now) session_start = datetime.now() session_expires = session_start + timedelta(hours=24) # Store in database for session tracking # user_sessions[from_number] = session_expires # Process message and respond response = process_whatsapp_message(from_number, body, media_url) # Reply within session whatsapp = TwilioWhatsApp() whatsapp.send_message(from_number, response) return "", 200def process_whatsapp_message(phone: str, text: str, media: str) -> str: """Process incoming message and generate response.""" text_lower = text.lower()
if "order status" in text_lower: return "Your order #1234 is out for delivery!" elif "support" in text_lower: return "A support agent will contact you shortly." else: return "Thanks for your message! Reply with 'order status' or 'support'."Send typing indicator (2025 feature)
def send_typing_indicator(to: str): """Let user know you're typing.""" # Requires Senders API setup pass anti_patterns:
- "Sending non-template messages outside 24-hour window"
- "Not tracking session windows per user"
- "Exceeding 1024 char limit for session messages"
- "Not handling template rejection errors"
-
id: webhook-handler-pattern name: Webhook Handler Pattern description: | Handle Twilio webhooks for delivery status, incoming messages, and call events. Critical: always validate X-Twilio-Signature.
Twilio sends webhooks for:
- Message status updates (queued → sent → delivered/failed)
- Incoming SMS/WhatsApp messages
- Call events (initiated, ringing, answered, completed)
- Recording/transcription ready when_to_use:
- "Tracking message delivery status"
- "Receiving incoming messages"
- "Call analytics and logging"
- "Voicemail transcription processing" implementation: | from flask import Flask, request, abort from twilio.request_validator import RequestValidator from functools import wraps import os import logging
app = Flask(name) logger = logging.getLogger(name)
def validate_twilio_signature(f): """ Validate that request came from Twilio. CRITICAL: Always use this for webhook endpoints. """ @wraps(f) def wrapper(*args, **kwargs): validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
# Build full URL (including query params) url = request.url # Get POST body as dict params = request.form.to_dict() # Get signature from header signature = request.headers.get("X-Twilio-Signature", "") if not validator.validate(url, params, signature): logger.warning(f"Invalid Twilio signature from {request.remote_addr}") abort(403) return f(*args, **kwargs) return wrapper@app.route("/webhooks/twilio/sms/status", methods=["POST"]) @validate_twilio_signature def sms_status_callback(): """ Handle SMS delivery status updates.
Status progression: queued → sending → sent → delivered Or: queued → sending → undelivered/failed """ message_sid = request.form.get("MessageSid") status = request.form.get("MessageStatus") error_code = request.form.get("ErrorCode") error_message = request.form.get("ErrorMessage") logger.info(f"SMS {message_sid}: {status}") if status == "delivered": # Message successfully delivered update_message_status(message_sid, "delivered") elif status == "undelivered": # Carrier rejected or other failure logger.error(f"SMS failed: {error_code} - {error_message}") handle_failed_message(message_sid, error_code, error_message) elif status == "failed": # Twilio couldn't send logger.error(f"SMS send failed: {error_code}") handle_failed_message(message_sid, error_code, error_message) return "", 200@app.route("/webhooks/twilio/sms/incoming", methods=["POST"]) @validate_twilio_signature def incoming_sms(): """ Handle incoming SMS messages. """ from_number = request.form.get("From") to_number = request.form.get("To") body = request.form.get("Body") num_media = int(request.form.get("NumMedia", 0))
# Handle media attachments media_urls = [] for i in range(num_media): media_urls.append(request.form.get(f"MediaUrl{i}")) # Check for opt-out keywords if body.strip().upper() in ["STOP", "UNSUBSCRIBE", "CANCEL"]: handle_opt_out(from_number) return "", 200 # Check for opt-in keywords if body.strip().upper() in ["START", "SUBSCRIBE"]: handle_opt_in(from_number) return "", 200 # Process message process_incoming_sms(from_number, body, media_urls) return "", 200@app.route("/webhooks/twilio/voice/status", methods=["POST"]) @validate_twilio_signature def voice_status_callback(): """Handle call status updates.""" call_sid = request.form.get("CallSid") status = request.form.get("CallStatus") duration = request.form.get("CallDuration") direction = request.form.get("Direction")
# Call statuses: initiated, ringing, in-progress, completed, busy, no-answer, canceled, failed logger.info(f"Call {call_sid}: {status} ({duration}s)") if status == "completed": # Call ended normally log_call_completion(call_sid, duration) elif status in ["busy", "no-answer", "canceled", "failed"]: # Call didn't connect handle_failed_call(call_sid, status) return "", 200Helper functions
def update_message_status(message_sid: str, status: str): """Update message status in database.""" pass
def handle_failed_message(message_sid: str, error_code: str, error_msg: str): """Handle failed message delivery.""" # Notify team, retry logic, etc. pass
def handle_opt_out(phone: str): """Handle user opting out of messages.""" # Mark user as opted out in database # IMPORTANT: Must respect this! pass
def handle_opt_in(phone: str): """Handle user opting back in.""" pass
def process_incoming_sms(from_phone: str, body: str, media: list): """Process incoming SMS message.""" pass
def log_call_completion(call_sid: str, duration: str): """Log completed call.""" pass
def handle_failed_call(call_sid: str, status: str): """Handle call that didn't connect.""" pass anti_patterns:
- "Not validating X-Twilio-Signature"
- "Exposing webhook URLs without authentication"
- "Not handling opt-out keywords (STOP)"
- "Blocking webhook response (should be fast)"
-
id: rate-limit-retry-pattern name: Rate Limit and Retry Pattern description: | Handle Twilio rate limits and implement proper retry logic.
Default limits:
- SMS: 80 messages per second (MPS)
- Voice: Varies by number type and region
- API calls: 100 requests per second
Error codes:
- 20429: Voice API rate limit
- 30429: Messaging API rate limit when_to_use:
- "High-volume messaging applications"
- "Bulk SMS campaigns"
- "Automated calling systems" implementation: | import time import random from functools import wraps from twilio.base.exceptions import TwilioRestException import logging
logger = logging.getLogger(name)
def exponential_backoff_retry( max_retries: int = 5, base_delay: float = 1.0, max_delay: float = 60.0, rate_limit_codes: list = [20429, 30429] ): """ Decorator for exponential backoff retry on rate limits.
Uses jitter to prevent thundering herd. """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): last_exception = None for attempt in range(max_retries + 1): try: return func(*args, **kwargs) except TwilioRestException as e: last_exception = e # Only retry on rate limit errors if e.code not in rate_limit_codes: raise if attempt == max_retries: logger.error(f"Max retries exceeded: {e}") raise # Calculate delay with jitter delay = min( base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay ) logger.warning( f"Rate limited (attempt {attempt + 1}/{max_retries}). " f"Retrying in {delay:.1f}s" ) time.sleep(delay) raise last_exception return wrapper return decoratorUsage
from twilio.rest import Client
client = Client(account_sid, auth_token)
@exponential_backoff_retry(max_retries=5) def send_sms(to: str, body: str): return client.messages.create( to=to, from_=from_number, body=body )
Bulk sending with rate limiting
import asyncio from asyncio import Semaphore
class RateLimitedSender: """ Send messages with built-in rate limiting. Stays under Twilio's 80 MPS limit. """
def __init__(self, client, from_number: str, mps: int = 50): self.client = client self.from_number = from_number self.mps = mps self.semaphore = Semaphore(mps) async def send_bulk(self, messages: list[dict]) -> list[dict]: """ Send messages with rate limiting. Args: messages: List of {"to": "+1...", "body": "..."} Returns: Results for each message """ tasks = [ self._send_with_limit(msg["to"], msg["body"]) for msg in messages ] return await asyncio.gather(*tasks, return_exceptions=True) async def _send_with_limit(self, to: str, body: str): """Send single message with semaphore-based rate limit.""" async with self.semaphore: try: # Use sync client in thread pool loop = asyncio.get_event_loop() result = await loop.run_in_executor( None, lambda: self.client.messages.create( to=to, from_=self.from_number, body=body ) ) return {"success": True, "sid": result.sid, "to": to} except TwilioRestException as e: return {"success": False, "error": str(e), "to": to} finally: # Delay to maintain rate limit await asyncio.sleep(1 / self.mps)Usage
async def send_campaign(): sender = RateLimitedSender(client, from_number, mps=50)
messages = [ {"to": "+14155551234", "body": "Hello!"}, {"to": "+14155555678", "body": "Hello!"}, # ... thousands of messages ] results = await sender.send_bulk(messages) successful = sum(1 for r in results if r.get("success")) print(f"Sent {successful}/{len(messages)} messages")anti_patterns:
- "Retrying immediately without backoff"
- "No jitter causing thundering herd"
- "Retrying non-rate-limit errors"
- "Exceeding Twilio's MPS limit"
============================================================================
DECISION FRAMEWORK
============================================================================
decision_framework: messaging_channel: question: "Which messaging channel should I use?" options: sms: when: "Universal reach, transactional messages" pros: "Works on all phones, no app needed" cons: "Per-message cost, carrier filtering, 160 char limit" cost: "$0.0075+ per message (US)"
whatsapp: when: "Rich media, international, interactive" pros: "Free inbound, rich media, 2B+ users" cons: "24-hour session window, template approval" cost: "Template: $0.005-0.08, Session: Free response" voice: when: "Urgent, accessibility, verification fallback" pros: "Higher attention, works without data" cons: "Higher cost, more intrusive" cost: "$0.013+ per minute"
verification_method: question: "How should I verify users?" options: sms_otp: when: "Most users, broad compatibility" security: "Blocks 96% of bulk phishing attacks" cost: "$0.05 per verification"
voice_otp: when: "SMS delivery issues, accessibility" security: "Same as SMS" cost: "$0.05 per verification" silent_network_auth: when: "Best UX, mobile apps" security: "Highest - carrier-verified" cost: "$0.05 per verification" whatsapp_otp: when: "WhatsApp-native apps, international" security: "Same as SMS" cost: "$0.05 per verification"
============================================================================
HANDOFFS
============================================================================
handoffs:
-
to: slack-bot-builder when: "Need Slack integration with Twilio" context: "Send SMS from Slack, notify channels on calls"
-
to: voice-agents when: "Building AI voice assistant" context: "Twilio provides telephony, voice agent handles conversation"
-
to: auth-specialist when: "Implementing full auth system" context: "Twilio Verify for 2FA, auth-specialist for broader auth"
-
to: devops when: "High-volume production deployment" context: "Rate limiting, monitoring, scaling webhooks"
============================================================================
QUICK REFERENCE
============================================================================
quick_reference: phone_format: correct: "+14155551234" incorrect: "415-555-1234, (415) 555-1234, 4155551234" note: "Always use E.164 format"
rate_limits: sms: "80 MPS default, 400 with approval" voice: "Varies by number type" api: "100 requests per second"
common_errors: 21610: "Recipient opted out (replied STOP)" 30003: "Phone unreachable (off, airplane mode)" 30005: "Unknown/invalid phone number" 30429: "Rate limit exceeded" 63016: "WhatsApp: Outside 24-hour window"
costs_usd: sms_outbound_us: "$0.0079" sms_inbound_us: "$0.0079" whatsapp_template: "$0.005-0.08" verify_sms: "$0.05" voice_outbound: "$0.013/min"
environment_variables: required: - "TWILIO_ACCOUNT_SID" - "TWILIO_AUTH_TOKEN" - "TWILIO_PHONE_NUMBER" optional: - "TWILIO_VERIFY_SID" - "TWILIO_WHATSAPP_NUMBER"