Twilio Communications 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. Patterns SMS Sending Pattern 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 mess…

\n return bool(re.match(pattern, phone))\n\n def send_sms(\n self,\n to: str,\n body: str,\n status_callback: str = None\n ) -> dict:\n \"\"\"\n Send an SMS message.\n\n Args:\n to: Recipient phone number in E.164 format\n body: Message text (160 chars = 1 segment)\n status_callback: URL for delivery status webhooks\n\n Returns:\n Message SID and status\n \"\"\"\n # Validate phone number format\n if not self.validate_e164(to):\n return {\n \"success\": False,\n \"error\": \"Phone number must be in E.164 format (+1234567890)\"\n }\n\n # Check message length (warn about segmentation)\n segment_count = (len(body) + 159) // 160\n if segment_count > 1:\n print(f\"Warning: Message will be sent as {segment_count} segments\")\n\n try:\n message = self.client.messages.create(\n to=to,\n from_=self.from_number,\n body=body,\n status_callback=status_callback\n )\n\n return {\n \"success\": True,\n \"message_sid\": message.sid,\n \"status\": message.status,\n \"segments\": segment_count\n }\n\n except TwilioRestException as e:\n return self._handle_error(e)\n\n def _handle_error(self, error: TwilioRestException) -> dict:\n \"\"\"Handle Twilio-specific errors.\"\"\"\n error_handlers = {\n 21610: \"Recipient has opted out. They must reply START.\",\n 21614: \"Invalid 'To' phone number format.\",\n 21211: \"'From' phone number is not valid.\",\n 30003: \"Phone is unreachable (off, airplane mode, no signal).\",\n 30005: \"Unknown destination (invalid number or landline).\",\n 30006: \"Landline or unreachable carrier.\",\n 30429: \"Rate limit exceeded. Implement exponential backoff.\",\n }\n\n return {\n \"success\": False,\n \"error_code\": error.code,\n \"error\": error_handlers.get(error.code, error.msg),\n \"details\": str(error)\n }\n\n# Usage\nsms = TwilioSMS()\nresult = sms.send_sms(\n to=\"+14155551234\",\n body=\"Your order #1234 has shipped!\",\n status_callback=\"https://your-app.com/webhooks/twilio/status\"\n)\n\n### Anti_patterns\n\n- Not validating E.164 format before sending\n- Hardcoding Twilio credentials in code\n- Ignoring delivery status callbacks\n- Not handling the opted-out (21610) error\n\n### Twilio Verify Pattern (2FA/OTP)\n\nUse Twilio Verify for phone number verification and 2FA.\nHandles code generation, delivery, rate limiting, and fraud prevention.\n\nKey benefits over DIY OTP:\n- Twilio manages code generation and expiration\n- Built-in fraud prevention (saved customers $82M+ blocking 747M attempts)\n- Handles rate limiting automatically\n- Multi-channel: SMS, Voice, Email, Push, WhatsApp\n\nGoogle found SMS 2FA blocks \"100% of automated bots, 96% of bulk\nphishing attacks, and 76% of targeted attacks.\"\n\n**When to use**: User phone number verification at signup,Two-factor authentication (2FA),Password reset verification,High-value transaction confirmation\n\nfrom twilio.rest import Client\nfrom twilio.base.exceptions import TwilioRestException\nimport os\nfrom enum import Enum\nfrom typing import Optional\n\nclass VerifyChannel(Enum):\n SMS = \"sms\"\n CALL = \"call\"\n EMAIL = \"email\"\n WHATSAPP = \"whatsapp\"\n\nclass TwilioVerify:\n \"\"\"\n Phone verification with Twilio Verify.\n Never store OTP codes - Twilio handles it.\n \"\"\"\n\n def __init__(self, verify_service_sid: str = None):\n self.client = Client(\n os.environ[\"TWILIO_ACCOUNT_SID\"],\n os.environ[\"TWILIO_AUTH_TOKEN\"]\n )\n # Create a Verify Service in Twilio Console first\n self.service_sid = verify_service_sid or os.environ[\"TWILIO_VERIFY_SID\"]\n\n def send_verification(\n self,\n to: str,\n channel: VerifyChannel = VerifyChannel.SMS,\n locale: str = \"en\"\n ) -> dict:\n \"\"\"\n Send verification code to phone/email.\n\n Args:\n to: Phone number (E.164) or email\n channel: SMS, call, email, or whatsapp\n locale: Language code for message\n\n Returns:\n Verification status\n \"\"\"\n try:\n verification = self.client.verify \\\n .v2 \\\n .services(self.service_sid) \\\n .verifications \\\n .create(\n to=to,\n channel=channel.value,\n locale=locale\n )\n\n return {\n \"success\": True,\n \"status\": verification.status, # \"pending\"\n \"channel\": channel.value,\n \"valid\": verification.valid\n }\n\n except TwilioRestException as e:\n return self._handle_verify_error(e)\n\n def check_verification(self, to: str, code: str) -> dict:\n \"\"\"\n Check if verification code is correct.\n\n Args:\n to: Phone number or email that received code\n code: The code entered by user\n\n Returns:\n Verification result\n \"\"\"\n try:\n check = self.client.verify \\\n .v2 \\\n .services(self.service_sid) \\\n .verification_checks \\\n .create(\n to=to,\n code=code\n )\n\n return {\n \"success\": True,\n \"valid\": check.status == \"approved\",\n \"status\": check.status # \"approved\" or \"pending\"\n }\n\n except TwilioRestException as e:\n # Code was wrong or expired\n return {\n \"success\": False,\n \"valid\": False,\n \"error\": str(e)\n }\n\n def _handle_verify_error(self, error: TwilioRestException) -> dict:\n \"\"\"Handle Verify-specific errors.\"\"\"\n error_handlers = {\n 60200: \"Invalid phone number format\",\n 60203: \"Max send attempts reached for this number\",\n 60205: \"Service not found - check VERIFY_SID\",\n 60223: \"Failed to create verification - carrier rejected\",\n }\n\n return {\n \"success\": False,\n \"error_code\": error.code,\n \"error\": error_handlers.get(error.code, error.msg)\n }\n\n# Usage Example - Signup Flow\nverify = TwilioVerify()\n\n# Step 1: User enters phone number\nresult = verify.send_verification(\"+14155551234\", VerifyChannel.SMS)\nif result[\"success\"]:\n print(\"Code sent! Check your phone.\")\n\n# Step 2: User enters the code they received\ncode = \"123456\" # From user input\ncheck = verify.check_verification(\"+14155551234\", code)\n\nif check[\"valid\"]:\n print(\"Phone verified! Create account.\")\nelse:\n print(\"Invalid code. Try again.\")\n\n# Best Practice: Offer voice fallback\nasync def verify_with_fallback(phone: str, max_attempts: int = 3):\n \"\"\"Verify with voice fallback if SMS fails.\"\"\"\n for attempt in range(max_attempts):\n channel = VerifyChannel.SMS if attempt == 0 else VerifyChannel.CALL\n result = verify.send_verification(phone, channel)\n\n if result[\"success\"]:\n return result\n\n # If SMS failed, wait and try voice\n if channel == VerifyChannel.SMS:\n await asyncio.sleep(30)\n continue\n\n return {\"success\": False, \"error\": \"All verification attempts failed\"}\n\n### Anti_patterns\n\n- Storing OTP codes in your database (Twilio handles this)\n- Not implementing rate limiting on your verify endpoint\n- Using same-code retries (let Verify generate new codes)\n- No fallback channel when SMS fails\n\n### TwiML IVR Pattern\n\nBuild Interactive Voice Response (IVR) systems using TwiML.\nTwiML (Twilio Markup Language) is XML that tells Twilio what to do\nwhen receiving calls.\n\nCore TwiML verbs:\n- \u003cSay>: Text-to-speech\n- \u003cPlay>: Play audio file\n- \u003cGather>: Collect keypad/speech input\n- \u003cDial>: Connect to another number\n- \u003cRecord>: Record caller's voice\n- \u003cRedirect>: Move to another TwiML endpoint\n\nKey insight: Twilio makes HTTP request to your webhook, you return\nTwiML, Twilio executes it. Stateless, so use URL params or sessions.\n\n**When to use**: Phone menu systems (press 1 for sales...),Automated customer support,Appointment reminders with confirmation,Voicemail systems\n\nfrom flask import Flask, request, Response\nfrom twilio.twiml.voice_response import VoiceResponse, Gather\nfrom twilio.request_validator import RequestValidator\nimport os\n\napp = Flask(__name__)\n\ndef validate_twilio_request(f):\n \"\"\"Decorator to validate requests are from Twilio.\"\"\"\n def wrapper(*args, **kwargs):\n validator = RequestValidator(os.environ[\"TWILIO_AUTH_TOKEN\"])\n\n # Get request details\n url = request.url\n params = request.form.to_dict()\n signature = request.headers.get(\"X-Twilio-Signature\", \"\")\n\n if not validator.validate(url, params, signature):\n return \"Invalid request\", 403\n\n return f(*args, **kwargs)\n wrapper.__name__ = f.__name__\n return wrapper\n\[email protected](\"/voice/incoming\", methods=[\"POST\"])\n@validate_twilio_request\ndef incoming_call():\n \"\"\"Handle incoming call with IVR menu.\"\"\"\n response = VoiceResponse()\n\n # Gather digits with timeout\n gather = Gather(\n num_digits=1,\n action=\"/voice/menu-selection\",\n method=\"POST\",\n timeout=5\n )\n gather.say(\n \"Welcome to Acme Corp. \"\n \"Press 1 for sales. \"\n \"Press 2 for support. \"\n \"Press 3 to leave a message.\"\n )\n response.append(gather)\n\n # If no input, repeat\n response.redirect(\"/voice/incoming\")\n\n return Response(str(response), mimetype=\"text/xml\")\n\[email protected](\"/voice/menu-selection\", methods=[\"POST\"])\n@validate_twilio_request\ndef menu_selection():\n \"\"\"Route based on menu selection.\"\"\"\n response = VoiceResponse()\n digit = request.form.get(\"Digits\", \"\")\n\n if digit == \"1\":\n # Transfer to sales\n response.say(\"Connecting you to sales.\")\n response.dial(os.environ[\"SALES_PHONE\"])\n\n elif digit == \"2\":\n # Transfer to support\n response.say(\"Connecting you to support.\")\n response.dial(os.environ[\"SUPPORT_PHONE\"])\n\n elif digit == \"3\":\n # Voicemail\n response.say(\"Please leave a message after the beep.\")\n response.record(\n action=\"/voice/voicemail-saved\",\n max_length=120,\n transcribe=True,\n transcribe_callback=\"/voice/transcription\"\n )\n\n else:\n response.say(\"Invalid selection.\")\n response.redirect(\"/voice/incoming\")\n\n return Response(str(response), mimetype=\"text/xml\")\n\[email protected](\"/voice/voicemail-saved\", methods=[\"POST\"])\n@validate_twilio_request\ndef voicemail_saved():\n \"\"\"Handle saved voicemail.\"\"\"\n response = VoiceResponse()\n\n recording_url = request.form.get(\"RecordingUrl\")\n recording_sid = request.form.get(\"RecordingSid\")\n\n # Save to database, notify team, etc.\n print(f\"Voicemail saved: {recording_url}\")\n\n response.say(\"Thank you. Goodbye.\")\n response.hangup()\n\n return Response(str(response), mimetype=\"text/xml\")\n\[email protected](\"/voice/transcription\", methods=[\"POST\"])\n@validate_twilio_request\ndef transcription_callback():\n \"\"\"Handle voicemail transcription.\"\"\"\n transcription = request.form.get(\"TranscriptionText\")\n recording_sid = request.form.get(\"RecordingSid\")\n\n # Save transcription, send to Slack, etc.\n print(f\"Transcription: {transcription}\")\n\n return \"\", 200\n\n# Outbound call example\nfrom twilio.rest import Client\n\ndef make_outbound_call(to: str, message: str):\n \"\"\"Make outbound call with custom TwiML.\"\"\"\n client = Client(\n os.environ[\"TWILIO_ACCOUNT_SID\"],\n os.environ[\"TWILIO_AUTH_TOKEN\"]\n )\n\n # TwiML Bin URL or your endpoint\n call = client.calls.create(\n to=to,\n from_=os.environ[\"TWILIO_PHONE_NUMBER\"],\n url=\"https://your-app.com/voice/outbound-message\",\n status_callback=\"https://your-app.com/voice/status\"\n )\n\n return call.sid\n\nif __name__ == \"__main__\":\n app.run(debug=True)\n\n### Anti_patterns\n\n- Not validating X-Twilio-Signature (security risk)\n- Returning non-XML responses to Twilio\n- Not handling timeout/no-input cases\n- Hardcoding phone numbers in TwiML\n\n### WhatsApp Business API Pattern\n\nSend and receive WhatsApp messages via Twilio API.\nUses the same Twilio Messages API as SMS with minor changes.\n\nKey WhatsApp rules:\n- 24-hour session window: Can only reply within 24 hours of user message\n- Template messages: Pre-approved templates for outside session window\n- Opt-in required: Users must explicitly consent to receive messages\n- Rate limit: 80 MPS default (up to 400 with approval)\n- Character limits: Non-template 1024 chars, templates ~550 chars\n\n**When to use**: Customer support with rich media,Order notifications with buttons,Marketing messages (with templates),Interactive flows (booking, surveys)\n\nfrom twilio.rest import Client\nfrom twilio.base.exceptions import TwilioRestException\nimport os\nfrom datetime import datetime, timedelta\nfrom typing import Optional\n\nclass TwilioWhatsApp:\n \"\"\"\n WhatsApp Business API via Twilio.\n Handles session windows and template messages.\n \"\"\"\n\n def __init__(self):\n self.client = Client(\n os.environ[\"TWILIO_ACCOUNT_SID\"],\n os.environ[\"TWILIO_AUTH_TOKEN\"]\n )\n # WhatsApp number format: whatsapp:+14155551234\n self.from_number = os.environ[\"TWILIO_WHATSAPP_NUMBER\"]\n\n def send_message(\n self,\n to: str,\n body: str,\n media_url: Optional[str] = None\n ) -> dict:\n \"\"\"\n Send WhatsApp message within 24-hour session.\n\n Args:\n to: Recipient number (E.164, without whatsapp: prefix)\n body: Message text (max 1024 chars for non-template)\n media_url: Optional image/document URL\n\n Returns:\n Message result\n \"\"\"\n # Format for WhatsApp\n to_whatsapp = f\"whatsapp:{to}\"\n from_whatsapp = f\"whatsapp:{self.from_number}\"\n\n try:\n message_params = {\n \"to\": to_whatsapp,\n \"from_\": from_whatsapp,\n \"body\": body\n }\n\n if media_url:\n message_params[\"media_url\"] = [media_url]\n\n message = self.client.messages.create(**message_params)\n\n return {\n \"success\": True,\n \"message_sid\": message.sid,\n \"status\": message.status\n }\n\n except TwilioRestException as e:\n return self._handle_whatsapp_error(e)\n\n def send_template_message(\n self,\n to: str,\n content_sid: str,\n content_variables: dict\n ) -> dict:\n \"\"\"\n Send pre-approved template message.\n Use this for messages outside 24-hour window.\n\n Content templates must be approved by WhatsApp first.\n Create them in Twilio Console > Content Template Builder.\n \"\"\"\n to_whatsapp = f\"whatsapp:{to}\"\n from_whatsapp = f\"whatsapp:{self.from_number}\"\n\n try:\n message = self.client.messages.create(\n to=to_whatsapp,\n from_=from_whatsapp,\n content_sid=content_sid,\n content_variables=content_variables\n )\n\n return {\n \"success\": True,\n \"message_sid\": message.sid,\n \"template\": True\n }\n\n except TwilioRestException as e:\n return self._handle_whatsapp_error(e)\n\n def _handle_whatsapp_error(self, error: TwilioRestException) -> dict:\n \"\"\"Handle WhatsApp-specific errors.\"\"\"\n error_handlers = {\n 63016: \"Outside 24-hour window. Use template message.\",\n 63018: \"Template not approved or doesn't exist.\",\n 63025: \"Too many template messages sent to this user.\",\n 63038: \"Rate limit exceeded for WhatsApp.\",\n }\n\n return {\n \"success\": False,\n \"error_code\": error.code,\n \"error\": error_handlers.get(error.code, error.msg)\n }\n\n# Flask webhook for incoming WhatsApp messages\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\[email protected](\"/webhooks/whatsapp\", methods=[\"POST\"])\ndef whatsapp_webhook():\n \"\"\"Handle incoming WhatsApp messages.\"\"\"\n from_number = request.form.get(\"From\", \"\").replace(\"whatsapp:\", \"\")\n body = request.form.get(\"Body\", \"\")\n media_url = request.form.get(\"MediaUrl0\") # First attachment\n\n # Track session start (24-hour window begins now)\n session_start = datetime.now()\n session_expires = session_start + timedelta(hours=24)\n\n # Store in database for session tracking\n # user_sessions[from_number] = session_expires\n\n # Process message and respond\n response = process_whatsapp_message(from_number, body, media_url)\n\n # Reply within session\n whatsapp = TwilioWhatsApp()\n whatsapp.send_message(from_number, response)\n\n return \"\", 200\n\ndef process_whatsapp_message(phone: str, text: str, media: str) -> str:\n \"\"\"Process incoming message and generate response.\"\"\"\n text_lower = text.lower()\n\n if \"order status\" in text_lower:\n return \"Your order #1234 is out for delivery!\"\n elif \"support\" in text_lower:\n return \"A support agent will contact you shortly.\"\n else:\n return \"Thanks for your message! Reply with 'order status' or 'support'.\"\n\n# Send typing indicator (2025 feature)\ndef send_typing_indicator(to: str):\n \"\"\"Let user know you're typing.\"\"\"\n # Requires Senders API setup\n pass\n\n### Anti_patterns\n\n- Sending non-template messages outside 24-hour window\n- Not tracking session windows per user\n- Exceeding 1024 char limit for session messages\n- Not handling template rejection errors\n\n### Webhook Handler Pattern\n\nHandle Twilio webhooks for delivery status, incoming messages,\nand call events. Critical: always validate X-Twilio-Signature.\n\nTwilio sends webhooks for:\n- Message status updates (queued → sent → delivered/failed)\n- Incoming SMS/WhatsApp messages\n- Call events (initiated, ringing, answered, completed)\n- Recording/transcription ready\n\n**When to use**: Tracking message delivery status,Receiving incoming messages,Call analytics and logging,Voicemail transcription processing\n\nfrom flask import Flask, request, abort\nfrom twilio.request_validator import RequestValidator\nfrom functools import wraps\nimport os\nimport logging\n\napp = Flask(__name__)\nlogger = logging.getLogger(__name__)\n\ndef validate_twilio_signature(f):\n \"\"\"\n Validate that request came from Twilio.\n CRITICAL: Always use this for webhook endpoints.\n \"\"\"\n @wraps(f)\n def wrapper(*args, **kwargs):\n validator = RequestValidator(os.environ[\"TWILIO_AUTH_TOKEN\"])\n\n # Build full URL (including query params)\n url = request.url\n\n # Get POST body as dict\n params = request.form.to_dict()\n\n # Get signature from header\n signature = request.headers.get(\"X-Twilio-Signature\", \"\")\n\n if not validator.validate(url, params, signature):\n logger.warning(f\"Invalid Twilio signature from {request.remote_addr}\")\n abort(403)\n\n return f(*args, **kwargs)\n return wrapper\n\[email protected](\"/webhooks/twilio/sms/status\", methods=[\"POST\"])\n@validate_twilio_signature\ndef sms_status_callback():\n \"\"\"\n Handle SMS delivery status updates.\n\n Status progression: queued → sending → sent → delivered\n Or: queued → sending → undelivered/failed\n \"\"\"\n message_sid = request.form.get(\"MessageSid\")\n status = request.form.get(\"MessageStatus\")\n error_code = request.form.get(\"ErrorCode\")\n error_message = request.form.get(\"ErrorMessage\")\n\n logger.info(f\"SMS {message_sid}: {status}\")\n\n if status == \"delivered\":\n # Message successfully delivered\n update_message_status(message_sid, \"delivered\")\n\n elif status == \"undelivered\":\n # Carrier rejected or other failure\n logger.error(f\"SMS failed: {error_code} - {error_message}\")\n handle_failed_message(message_sid, error_code, error_message)\n\n elif status == \"failed\":\n # Twilio couldn't send\n logger.error(f\"SMS send failed: {error_code}\")\n handle_failed_message(message_sid, error_code, error_message)\n\n return \"\", 200\n\[email protected](\"/webhooks/twilio/sms/incoming\", methods=[\"POST\"])\n@validate_twilio_signature\ndef incoming_sms():\n \"\"\"\n Handle incoming SMS messages.\n \"\"\"\n from_number = request.form.get(\"From\")\n to_number = request.form.get(\"To\")\n body = request.form.get(\"Body\")\n num_media = int(request.form.get(\"NumMedia\", 0))\n\n # Handle media attachments\n media_urls = []\n for i in range(num_media):\n media_urls.append(request.form.get(f\"MediaUrl{i}\"))\n\n # Check for opt-out keywords\n if body.strip().upper() in [\"STOP\", \"UNSUBSCRIBE\", \"CANCEL\"]:\n handle_opt_out(from_number)\n return \"\", 200\n\n # Check for opt-in keywords\n if body.strip().upper() in [\"START\", \"SUBSCRIBE\"]:\n handle_opt_in(from_number)\n return \"\", 200\n\n # Process message\n process_incoming_sms(from_number, body, media_urls)\n\n return \"\", 200\n\[email protected](\"/webhooks/twilio/voice/status\", methods=[\"POST\"])\n@validate_twilio_signature\ndef voice_status_callback():\n \"\"\"Handle call status updates.\"\"\"\n call_sid = request.form.get(\"CallSid\")\n status = request.form.get(\"CallStatus\")\n duration = request.form.get(\"CallDuration\")\n direction = request.form.get(\"Direction\")\n\n # Call statuses: initiated, ringing, in-progress, completed, busy, no-answer, canceled, failed\n\n logger.info(f\"Call {call_sid}: {status} ({duration}s)\")\n\n if status == \"completed\":\n # Call ended normally\n log_call_completion(call_sid, duration)\n\n elif status in [\"busy\", \"no-answer\", \"canceled\", \"failed\"]:\n # Call didn't connect\n handle_failed_call(call_sid, status)\n\n return \"\", 200\n\n# Helper functions\ndef update_message_status(message_sid: str, status: str):\n \"\"\"Update message status in database.\"\"\"\n pass\n\ndef handle_failed_message(message_sid: str, error_code: str, error_msg: str):\n \"\"\"Handle failed message delivery.\"\"\"\n # Notify team, retry logic, etc.\n pass\n\ndef handle_opt_out(phone: str):\n \"\"\"Handle user opting out of messages.\"\"\"\n # Mark user as opted out in database\n # IMPORTANT: Must respect this!\n pass\n\ndef handle_opt_in(phone: str):\n \"\"\"Handle user opting back in.\"\"\"\n pass\n\ndef process_incoming_sms(from_phone: str, body: str, media: list):\n \"\"\"Process incoming SMS message.\"\"\"\n pass\n\ndef log_call_completion(call_sid: str, duration: str):\n \"\"\"Log completed call.\"\"\"\n pass\n\ndef handle_failed_call(call_sid: str, status: str):\n \"\"\"Handle call that didn't connect.\"\"\"\n pass\n\n### Anti_patterns\n\n- Not validating X-Twilio-Signature\n- Exposing webhook URLs without authentication\n- Not handling opt-out keywords (STOP)\n- Blocking webhook response (should be fast)\n\n### Rate Limit and Retry Pattern\n\nHandle Twilio rate limits and implement proper retry logic.\n\nDefault limits:\n- SMS: 80 messages per second (MPS)\n- Voice: Varies by number type and region\n- API calls: 100 requests per second\n\nError codes:\n- 20429: Voice API rate limit\n- 30429: Messaging API rate limit\n\n**When to use**: High-volume messaging applications,Bulk SMS campaigns,Automated calling systems\n\nimport time\nimport random\nfrom functools import wraps\nfrom twilio.base.exceptions import TwilioRestException\nimport logging\n\nlogger = logging.getLogger(__name__)\n\ndef exponential_backoff_retry(\n max_retries: int = 5,\n base_delay: float = 1.0,\n max_delay: float = 60.0,\n rate_limit_codes: list = [20429, 30429]\n):\n \"\"\"\n Decorator for exponential backoff retry on rate limits.\n\n Uses jitter to prevent thundering herd.\n \"\"\"\n def decorator(func):\n @wraps(func)\n def wrapper(*args, **kwargs):\n last_exception = None\n\n for attempt in range(max_retries + 1):\n try:\n return func(*args, **kwargs)\n\n except TwilioRestException as e:\n last_exception = e\n\n # Only retry on rate limit errors\n if e.code not in rate_limit_codes:\n raise\n\n if attempt == max_retries:\n logger.error(f\"Max retries exceeded: {e}\")\n raise\n\n # Calculate delay with jitter\n delay = min(\n base_delay * (2 ** attempt) + random.uniform(0, 1),\n max_delay\n )\n\n logger.warning(\n f\"Rate limited (attempt {attempt + 1}/{max_retries}). \"\n f\"Retrying in {delay:.1f}s\"\n )\n time.sleep(delay)\n\n raise last_exception\n\n return wrapper\n return decorator\n\n# Usage\nfrom twilio.rest import Client\n\nclient = Client(account_sid, auth_token)\n\n@exponential_backoff_retry(max_retries=5)\ndef send_sms(to: str, body: str):\n return client.messages.create(\n to=to,\n from_=from_number,\n body=body\n )\n\n# Bulk sending with rate limiting\nimport asyncio\nfrom asyncio import Semaphore\n\nclass RateLimitedSender:\n \"\"\"\n Send messages with built-in rate limiting.\n Stays under Twilio's 80 MPS limit.\n \"\"\"\n\n def __init__(self, client, from_number: str, mps: int = 50):\n self.client = client\n self.from_number = from_number\n self.mps = mps\n self.semaphore = Semaphore(mps)\n\n async def send_bulk(self, messages: list[dict]) -> list[dict]:\n \"\"\"\n Send messages with rate limiting.\n\n Args:\n messages: List of {\"to\": \"+1...\", \"body\": \"...\"}\n\n Returns:\n Results for each message\n \"\"\"\n tasks = [\n self._send_with_limit(msg[\"to\"], msg[\"body\"])\n for msg in messages\n ]\n\n return await asyncio.gather(*tasks, return_exceptions=True)\n\n async def _send_with_limit(self, to: str, body: str):\n \"\"\"Send single message with semaphore-based rate limit.\"\"\"\n async with self.semaphore:\n try:\n # Use sync client in thread pool\n loop = asyncio.get_event_loop()\n result = await loop.run_in_executor(\n None,\n lambda: self.client.messages.create(\n to=to,\n from_=self.from_number,\n body=body\n )\n )\n return {\"success\": True, \"sid\": result.sid, \"to\": to}\n\n except TwilioRestException as e:\n return {\"success\": False, \"error\": str(e), \"to\": to}\n\n finally:\n # Delay to maintain rate limit\n await asyncio.sleep(1 / self.mps)\n\n# Usage\nasync def send_campaign():\n sender = RateLimitedSender(client, from_number, mps=50)\n\n messages = [\n {\"to\": \"+14155551234\", \"body\": \"Hello!\"},\n {\"to\": \"+14155555678\", \"body\": \"Hello!\"},\n # ... thousands of messages\n ]\n\n results = await sender.send_bulk(messages)\n\n successful = sum(1 for r in results if r.get(\"success\"))\n print(f\"Sent {successful}/{len(messages)} messages\")\n\n### Anti_patterns\n\n- Retrying immediately without backoff\n- No jitter causing thundering herd\n- Retrying non-rate-limit errors\n- Exceeding Twilio's MPS limit\n\n## Sharp Edges\n\n### Sending to Users Who Opted Out (Error 21610)\n\nSeverity: HIGH\n\nSituation: Sending SMS to a phone number\n\nSymptoms:\nMessage fails with error code 21610. Twilio rejects the message.\nUser never receives the SMS. Same number worked before.\n\nWhy this breaks:\nThe recipient replied \"STOP\" (or UNSUBSCRIBE, CANCEL, etc.) to a previous\nmessage from your number. Twilio automatically honors opt-outs and blocks\nfurther messages to that number from your account.\n\nThis is legally required for US messaging (TCPA, CTIA guidelines).\nYou cannot override this - the user must reply \"START\" to opt back in.\n\nRecommended fix:\n\n## Track opt-out status in your database\n\n```python\n# In your webhook handler\[email protected](\"/webhooks/sms/incoming\", methods=[\"POST\"])\ndef incoming_sms():\n from_number = request.form.get(\"From\")\n body = request.form.get(\"Body\", \"\").strip().upper()\n\n # Standard opt-out keywords\n if body in [\"STOP\", \"UNSUBSCRIBE\", \"CANCEL\", \"END\", \"QUIT\"]:\n mark_user_opted_out(from_number)\n return \"\", 200\n\n # Standard opt-in keywords\n if body in [\"START\", \"SUBSCRIBE\", \"YES\", \"UNSTOP\"]:\n mark_user_opted_in(from_number)\n return \"\", 200\n\n # Process other messages...\n\n# Before sending\ndef send_sms_safe(to: str, body: str):\n if is_user_opted_out(to):\n return {\"success\": False, \"error\": \"User has opted out\"}\n\n try:\n return send_sms(to, body)\n except TwilioRestException as e:\n if e.code == 21610:\n # Update database - they opted out via carrier\n mark_user_opted_out(to)\n raise\n```\n\n## Include opt-out instructions\nAdd \"Reply STOP to unsubscribe\" to marketing messages.\n\n### Phone Unreachable But Valid (Error 30003)\n\nSeverity: MEDIUM\n\nSituation: Sending SMS to a mobile number\n\nSymptoms:\nMessage fails with error 30003. Number was valid and worked before.\nIntermittent - sometimes works, sometimes fails.\n\nWhy this breaks:\nError 30003 means \"Unreachable destination handset.\" The phone exists but\ncan't receive messages right now. Common causes:\n- Phone powered off\n- Airplane mode\n- Out of signal range\n- Carrier network issues\n- Phone storage full\n\nUnlike 30006 (permanent unreachable), 30003 is usually temporary.\n\nRecommended fix:\n\n## Implement retry logic for transient failures\n\n```python\nTRANSIENT_ERRORS = [30003, 30008, 30009] # Retriable errors\n\nasync def send_with_retry(to: str, body: str, max_retries: int = 3):\n for attempt in range(max_retries):\n result = send_sms(to, body)\n\n if result[\"success\"]:\n return result\n\n if result.get(\"error_code\") not in TRANSIENT_ERRORS:\n # Don't retry permanent failures\n return result\n\n # Exponential backoff: 5min, 15min, 45min\n delay = 300 * (3 ** attempt)\n await asyncio.sleep(delay)\n\n return {\"success\": False, \"error\": \"Max retries exceeded\"}\n```\n\n## Provide fallback channel\n\n```python\nasync def notify_user(user, message):\n # Try SMS first\n result = await send_sms(user.phone, message)\n\n if result.get(\"error_code\") == 30003:\n # Phone unreachable - try email\n await send_email(user.email, message)\n return {\"channel\": \"email\", \"status\": \"sent\"}\n\n return {\"channel\": \"sms\", \"status\": result[\"status\"]}\n```\n\n### Messages Blocked by Carrier Filtering\n\nSeverity: HIGH\n\nSituation: Sending SMS to US phone numbers\n\nSymptoms:\nMessages show as \"sent\" but never \"delivered.\" No error from Twilio.\nUsers say they never received the message. Pattern in specific carriers\nor message content.\n\nWhy this breaks:\nUS carriers (Verizon, AT&T, T-Mobile) aggressively filter SMS for spam.\nYour message might be blocked if:\n- Contains URLs (especially short URLs or unknown domains)\n- Looks like phishing (urgent, account, verify, click now)\n- High volume from same number\n- Not using registered A2P 10DLC\n- Low sender reputation\n\nCarriers don't tell Twilio why messages are filtered - they just\nsilently drop them.\n\nRecommended fix:\n\n## Register for A2P 10DLC (US requirement)\n\n```\n1. Go to Twilio Console > Messaging > Trust Hub\n2. Register your business brand\n3. Create a messaging campaign (describes use case)\n4. Wait for approval (can take days)\n5. Associate phone numbers with campaign\n```\n\n## Message content best practices\n\n```python\ndef sanitize_message(text: str) -> str:\n \"\"\"Make message less likely to be filtered.\"\"\"\n # Avoid URL shorteners - use full domain\n # Avoid spam trigger words\n # Keep it conversational, not promotional\n\n # Example: Instead of this\n bad = \"URGENT: Verify your account now! Click: bit.ly/abc\"\n\n # Do this\n good = \"Hi! Your order #1234 is ready. Questions? Reply here.\"\n\n return text\n\n# Use toll-free or short code for high volume\n# 10DLC is for \u003c10K msg/day\n# Toll-free: up to 10K msg/day\n# Short code: 100K+ msg/day\n```\n\n## Monitor delivery rates\n\n```python\ndef track_delivery_rate():\n sent = get_messages_with_status(\"sent\")\n delivered = get_messages_with_status(\"delivered\")\n\n rate = len(delivered) / len(sent) * 100\n\n if rate \u003c 95:\n alert_team(f\"Delivery rate dropped to {rate}%\")\n```\n\n### Not Validating Webhook Signatures\n\nSeverity: CRITICAL\n\nSituation: Receiving Twilio webhook callbacks\n\nSymptoms:\nAttackers send fake webhooks to your endpoint. Fraudulent transactions\nprocessed. Spoofed incoming messages trigger actions.\n\nWhy this breaks:\nTwilio signs all webhook requests with X-Twilio-Signature header.\nIf you don't validate this, anyone who knows your webhook URL can\nsend fake requests pretending to be Twilio.\n\nThis can lead to:\n- Fake message delivery confirmations\n- Spoofed incoming messages\n- Fraudulent verification approvals\n\nRecommended fix:\n\n## ALWAYS validate the signature\n\n```python\nfrom twilio.request_validator import RequestValidator\nfrom flask import Flask, request, abort\nfrom functools import wraps\nimport os\n\ndef require_twilio_signature(f):\n \"\"\"Decorator to validate Twilio webhook requests.\"\"\"\n @wraps(f)\n def wrapper(*args, **kwargs):\n validator = RequestValidator(os.environ[\"TWILIO_AUTH_TOKEN\"])\n\n # Full URL including query string\n url = request.url\n\n # POST body as dict\n params = request.form.to_dict()\n\n # Signature header\n signature = request.headers.get(\"X-Twilio-Signature\", \"\")\n\n if not validator.validate(url, params, signature):\n abort(403)\n\n return f(*args, **kwargs)\n return wrapper\n\[email protected](\"/webhooks/twilio\", methods=[\"POST\"])\n@require_twilio_signature # ALWAYS use this\ndef twilio_webhook():\n # Safe to process\n pass\n```\n\n## Common validation gotchas\n\n```python\n# URL must match EXACTLY what Twilio called\n# If behind proxy, you might need:\nurl = request.headers.get(\"X-Forwarded-Proto\", \"http\") + \"://\" + \\\n request.headers.get(\"X-Forwarded-Host\", request.host) + \\\n request.path\n\n# If using ngrok, URL changes each restart\n# Use consistent URL in production\n```\n\n### WhatsApp Message Outside 24-Hour Window (Error 63016)\n\nSeverity: HIGH\n\nSituation: Sending WhatsApp message to a user\n\nSymptoms:\nMessage fails with error 63016. \"Message is outside the allowed window.\"\nTemplate messages work, but regular messages fail.\n\nWhy this breaks:\nWhatsApp has strict rules about unsolicited messages:\n- Users must message you first\n- You can only reply within 24 hours of their last message\n- After 24 hours, you must use pre-approved template messages\n\nThis prevents spam and maintains WhatsApp's trust as a platform.\n\nRecommended fix:\n\n## Track session windows per user\n\n```python\nfrom datetime import datetime, timedelta\n\nclass WhatsAppSession:\n def __init__(self, redis_client):\n self.redis = redis_client\n self.window_hours = 24\n\n def start_session(self, phone: str):\n \"\"\"Start/refresh 24-hour session on incoming message.\"\"\"\n key = f\"wa_session:{phone}\"\n expires = datetime.now() + timedelta(hours=self.window_hours)\n self.redis.set(key, expires.isoformat(), ex=self.window_hours * 3600)\n\n def can_send_freeform(self, phone: str) -> bool:\n \"\"\"Check if we can send non-template message.\"\"\"\n key = f\"wa_session:{phone}\"\n expires_str = self.redis.get(key)\n\n if not expires_str:\n return False\n\n expires = datetime.fromisoformat(expires_str)\n return datetime.now() \u003c expires\n\n def send_message(self, phone: str, body: str, template_sid: str = None):\n \"\"\"Send message, using template if outside window.\"\"\"\n if self.can_send_freeform(phone):\n return send_whatsapp_message(phone, body)\n elif template_sid:\n return send_whatsapp_template(phone, template_sid)\n else:\n return {\n \"success\": False,\n \"error\": \"Outside session window, template required\"\n }\n```\n\n## Incoming message webhook\n\n```python\[email protected](\"/webhooks/whatsapp\", methods=[\"POST\"])\ndef whatsapp_incoming():\n from_phone = request.form.get(\"From\").replace(\"whatsapp:\", \"\")\n\n # Start/refresh session\n session.start_session(from_phone)\n\n # Process message...\n```\n\n## Create approved templates for common messages\n\n```\n1. Twilio Console > Content Template Builder\n2. Create template with {{1}} placeholders\n3. Submit for WhatsApp approval (takes 24-48 hours)\n4. Use content_sid to send\n```\n\n### Exposed Account SID or Auth Token\n\nSeverity: CRITICAL\n\nSituation: Deploying Twilio integration\n\nSymptoms:\nUnauthorized charges on Twilio account. Messages sent you didn't send.\nPhone numbers purchased without authorization.\n\nWhy this breaks:\nIf attackers get your Account SID + Auth Token, they have FULL access\nto your Twilio account. They can:\n- Send messages (charging your account)\n- Buy phone numbers\n- Access call recordings\n- Modify your configuration\n\nCommon exposure points:\n- Hardcoded in source code (pushed to GitHub)\n- In client-side JavaScript\n- In Docker images\n- In logs\n\nRecommended fix:\n\n## Never hardcode credentials\n\n```python\n# BAD - never do this\nclient = Client(\"AC1234...\", \"abc123...\")\n\n# GOOD - environment variables\nclient = Client(\n os.environ[\"TWILIO_ACCOUNT_SID\"],\n os.environ[\"TWILIO_AUTH_TOKEN\"]\n)\n\n# GOOD - secrets manager\nfrom aws_secretsmanager import get_secret\ncreds = get_secret(\"twilio-credentials\")\nclient = Client(creds[\"sid\"], creds[\"token\"])\n```\n\n## Use API Key instead of Auth Token\n\n```python\n# Auth Token has full account access\n# API Keys can be scoped and revoked\n\n# Create API Key in Twilio Console\nclient = Client(\n os.environ[\"TWILIO_API_KEY_SID\"],\n os.environ[\"TWILIO_API_KEY_SECRET\"],\n os.environ[\"TWILIO_ACCOUNT_SID\"]\n)\n\n# If compromised, revoke just that key\n```\n\n## Rotate tokens immediately if exposed\n\n```\n1. Twilio Console > Account > API credentials\n2. Rotate Auth Token\n3. Update all deployments with new token\n4. Review account activity for unauthorized use\n```\n\n### Verify Rate Limit Exceeded (Error 60203)\n\nSeverity: MEDIUM\n\nSituation: Sending verification codes\n\nSymptoms:\nVerification request fails with error 60203.\n\"Max send attempts reached for this phone number.\"\n\nWhy this breaks:\nTwilio Verify has built-in rate limits to prevent abuse:\n- 5 verification attempts per phone number per service per 10 minutes\n- Helps prevent SMS pumping fraud\n- Protects against brute-force attacks\n\nIf users legitimately need more attempts, you may have UX issues.\n\nRecommended fix:\n\n## Implement application-level rate limiting too\n\n```python\nfrom datetime import datetime, timedelta\nimport redis\n\nclass VerifyRateLimiter:\n def __init__(self, redis_client):\n self.redis = redis_client\n # Stricter than Twilio's limit\n self.max_attempts = 3\n self.window_minutes = 10\n\n def can_request(self, phone: str) -> bool:\n key = f\"verify_rate:{phone}\"\n attempts = self.redis.get(key)\n\n if attempts and int(attempts) >= self.max_attempts:\n return False\n\n return True\n\n def record_attempt(self, phone: str):\n key = f\"verify_rate:{phone}\"\n pipe = self.redis.pipeline()\n pipe.incr(key)\n pipe.expire(key, self.window_minutes * 60)\n pipe.execute()\n\n def get_wait_time(self, phone: str) -> int:\n \"\"\"Return seconds until user can request again.\"\"\"\n key = f\"verify_rate:{phone}\"\n ttl = self.redis.ttl(key)\n return max(0, ttl)\n\n# Usage\nlimiter = VerifyRateLimiter(redis_client)\n\[email protected](\"/verify/send\", methods=[\"POST\"])\ndef send_verification():\n phone = request.json[\"phone\"]\n\n if not limiter.can_request(phone):\n wait = limiter.get_wait_time(phone)\n return {\n \"error\": f\"Too many attempts. Try again in {wait} seconds.\"\n }, 429\n\n result = twilio_verify.send_verification(phone)\n\n if result[\"success\"]:\n limiter.record_attempt(phone)\n\n return result\n```\n\n## Provide clear user feedback\n\n```python\n# Show remaining attempts\n# Show countdown timer\n# Offer alternative (voice call, email)\n```\n\n## Validation Checks\n\n### Hardcoded Twilio Credentials\n\nSeverity: ERROR\n\nTwilio credentials must never be hardcoded\n\nMessage: Hardcoded Twilio SID detected. Use environment variables.\n\n### Auth Token in Source Code\n\nSeverity: ERROR\n\nAuth tokens should be in environment variables\n\nMessage: Hardcoded auth token. Use os.environ['TWILIO_AUTH_TOKEN'].\n\n### Webhook Without Signature Validation\n\nSeverity: ERROR\n\nTwilio webhooks must validate X-Twilio-Signature\n\nMessage: Webhook without signature validation. Add RequestValidator check.\n\n### Twilio Credentials in Client-Side Code\n\nSeverity: ERROR\n\nNever expose Twilio credentials to browsers\n\nMessage: Twilio credentials exposed client-side. Only use server-side.\n\n### No E.164 Phone Number Validation\n\nSeverity: WARNING\n\nPhone numbers should be validated before sending\n\nMessage: Sending to phone without E.164 validation.\n\n### Hardcoded Phone Numbers\n\nSeverity: WARNING\n\nPhone numbers should come from config or database\n\nMessage: Hardcoded phone number. Use config or environment variable.\n\n### No Twilio Exception Handling\n\nSeverity: WARNING\n\nTwilio calls should handle TwilioRestException\n\nMessage: Twilio API call without error handling. Catch TwilioRestException.\n\n### Not Handling Specific Error Codes\n\nSeverity: INFO\n\nHandle common Twilio error codes specifically\n\nMessage: Consider handling specific error codes (21610, 30003, etc.).\n\n### No Opt-Out Keyword Handling\n\nSeverity: WARNING\n\nSMS systems must handle STOP/UNSUBSCRIBE keywords\n\nMessage: No opt-out handling. Check for STOP/UNSUBSCRIBE keywords.\n\n### Not Checking Opt-Out Before Sending\n\nSeverity: WARNING\n\nCheck if user has opted out before sending SMS\n\nMessage: Consider checking opt-out status before sending.\n\n## Collaboration\n\n### Delegation Triggers\n\n- user needs AI voice assistant -> voice-agents (Twilio provides telephony, voice-agents skill for AI conversation)\n- user needs Slack notifications -> slack-bot-builder (Integrate SMS alerts with Slack notifications)\n- user needs full auth system -> auth-specialist (Twilio Verify is one component of broader auth)\n- user needs workflow automation -> workflow-automation (Trigger SMS/calls from automated workflows)\n- user needs high-volume messaging -> devops (Scale webhooks, monitor delivery rates)\n\n## When to Use\n- User mentions or implies: twilio\n- User mentions or implies: send SMS\n- User mentions or implies: text message\n- User mentions or implies: voice call\n- User mentions or implies: phone verification\n- User mentions or implies: 2FA SMS\n- User mentions or implies: WhatsApp API\n- User mentions or implies: programmable messaging\n- User mentions or implies: IVR system\n- User mentions or implies: TwiML\n- User mentions or implies: phone number verification\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.\n---","attachment_filenames":[],"attachments":[],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Twilio Communications","type":"text"}]},{"type":"paragraph","content":[{"text":"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.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"SMS Sending Pattern","type":"text"}]},{"type":"paragraph","content":[{"text":"Basic pattern for sending SMS messages with Twilio. Handles the fundamentals: phone number formatting, message delivery, and delivery status callbacks.","type":"text"}]},{"type":"paragraph","content":[{"text":"Key considerations:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Phone numbers must be in E.164 format (+1234567890)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Default rate limit: 80 messages per second (MPS)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Messages over 160 characters are split (and cost more)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Carrier filtering can block messages (especially to US numbers)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When to use","type":"text","marks":[{"type":"strong"}]},{"text":": Sending notifications to users,Transactional messages (order confirmations, shipping),Alerts and reminders","type":"text"}]},{"type":"paragraph","content":[{"text":"from twilio.rest import Client from twilio.base.exceptions import TwilioRestException import os import re","type":"text"}]},{"type":"paragraph","content":[{"text":"class TwilioSMS: \"\"\" SMS sending with proper error handling and validation. \"\"\"","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"def __init__(self):\n self.client = Client(\n os.environ[\"TWILIO_ACCOUNT_SID\"],\n os.environ[\"TWILIO_AUTH_TOKEN\"]\n )\n self.from_number = os.environ[\"TWILIO_PHONE_NUMBER\"]\n\ndef validate_e164(self, phone: str) -> bool:\n \"\"\"Validate phone number is in E.164 format.\"\"\"\n pattern = r'^\\+[1-9]\\d{1,14}

Twilio Communications 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. Patterns SMS Sending Pattern 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 mess…

\n return bool(re.match(pattern, phone))\n\ndef send_sms(\n self,\n to: str,\n body: str,\n status_callback: str = None\n) -> dict:\n \"\"\"\n Send an SMS message.\n\n Args:\n to: Recipient phone number in E.164 format\n body: Message text (160 chars = 1 segment)\n status_callback: URL for delivery status webhooks\n\n Returns:\n Message SID and status\n \"\"\"\n # Validate phone number format\n if not self.validate_e164(to):\n return {\n \"success\": False,\n \"error\": \"Phone number must be in E.164 format (+1234567890)\"\n }\n\n # Check message length (warn about segmentation)\n segment_count = (len(body) + 159) // 160\n if segment_count > 1:\n print(f\"Warning: Message will be sent as {segment_count} segments\")\n\n try:\n message = self.client.messages.create(\n to=to,\n from_=self.from_number,\n body=body,\n status_callback=status_callback\n )\n\n return {\n \"success\": True,\n \"message_sid\": message.sid,\n \"status\": message.status,\n \"segments\": segment_count\n }\n\n except TwilioRestException as e:\n return self._handle_error(e)\n\ndef _handle_error(self, error: TwilioRestException) -> dict:\n \"\"\"Handle Twilio-specific errors.\"\"\"\n error_handlers = {\n 21610: \"Recipient has opted out. They must reply START.\",\n 21614: \"Invalid 'To' phone number format.\",\n 21211: \"'From' phone number is not valid.\",\n 30003: \"Phone is unreachable (off, airplane mode, no signal).\",\n 30005: \"Unknown destination (invalid number or landline).\",\n 30006: \"Landline or unreachable carrier.\",\n 30429: \"Rate limit exceeded. Implement exponential backoff.\",\n }\n\n return {\n \"success\": False,\n \"error_code\": error.code,\n \"error\": error_handlers.get(error.code, error.msg),\n \"details\": str(error)\n }","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Usage","type":"text"}]},{"type":"paragraph","content":[{"text":"sms = TwilioSMS() result = sms.send_sms( to=\"+14155551234\", body=\"Your order #1234 has shipped!\", status_callback=\"https://your-app.com/webhooks/twilio/status\" )","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Anti_patterns","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Not validating E.164 format before sending","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hardcoding Twilio credentials in code","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ignoring delivery status callbacks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Not handling the opted-out (21610) error","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Twilio Verify Pattern (2FA/OTP)","type":"text"}]},{"type":"paragraph","content":[{"text":"Use Twilio Verify for phone number verification and 2FA. Handles code generation, delivery, rate limiting, and fraud prevention.","type":"text"}]},{"type":"paragraph","content":[{"text":"Key benefits over DIY OTP:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Twilio manages code generation and expiration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Built-in fraud prevention (saved customers $82M+ blocking 747M attempts)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Handles rate limiting automatically","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Multi-channel: SMS, Voice, Email, Push, WhatsApp","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Google found SMS 2FA blocks \"100% of automated bots, 96% of bulk phishing attacks, and 76% of targeted attacks.\"","type":"text"}]},{"type":"paragraph","content":[{"text":"When to use","type":"text","marks":[{"type":"strong"}]},{"text":": User phone number verification at signup,Two-factor authentication (2FA),Password reset verification,High-value transaction confirmation","type":"text"}]},{"type":"paragraph","content":[{"text":"from twilio.rest import Client from twilio.base.exceptions import TwilioRestException import os from enum import Enum from typing import Optional","type":"text"}]},{"type":"paragraph","content":[{"text":"class VerifyChannel(Enum): SMS = \"sms\" CALL = \"call\" EMAIL = \"email\" WHATSAPP = \"whatsapp\"","type":"text"}]},{"type":"paragraph","content":[{"text":"class TwilioVerify: \"\"\" Phone verification with Twilio Verify. Never store OTP codes - Twilio handles it. \"\"\"","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"def __init__(self, verify_service_sid: str = None):\n self.client = Client(\n os.environ[\"TWILIO_ACCOUNT_SID\"],\n os.environ[\"TWILIO_AUTH_TOKEN\"]\n )\n # Create a Verify Service in Twilio Console first\n self.service_sid = verify_service_sid or os.environ[\"TWILIO_VERIFY_SID\"]\n\ndef send_verification(\n self,\n to: str,\n channel: VerifyChannel = VerifyChannel.SMS,\n locale: str = \"en\"\n) -> dict:\n \"\"\"\n Send verification code to phone/email.\n\n Args:\n to: Phone number (E.164) or email\n channel: SMS, call, email, or whatsapp\n locale: Language code for message\n\n Returns:\n Verification status\n \"\"\"\n try:\n verification = self.client.verify \\\n .v2 \\\n .services(self.service_sid) \\\n .verifications \\\n .create(\n to=to,\n channel=channel.value,\n locale=locale\n )\n\n return {\n \"success\": True,\n \"status\": verification.status, # \"pending\"\n \"channel\": channel.value,\n \"valid\": verification.valid\n }\n\n except TwilioRestException as e:\n return self._handle_verify_error(e)\n\ndef check_verification(self, to: str, code: str) -> dict:\n \"\"\"\n Check if verification code is correct.\n\n Args:\n to: Phone number or email that received code\n code: The code entered by user\n\n Returns:\n Verification result\n \"\"\"\n try:\n check = self.client.verify \\\n .v2 \\\n .services(self.service_sid) \\\n .verification_checks \\\n .create(\n to=to,\n code=code\n )\n\n return {\n \"success\": True,\n \"valid\": check.status == \"approved\",\n \"status\": check.status # \"approved\" or \"pending\"\n }\n\n except TwilioRestException as e:\n # Code was wrong or expired\n return {\n \"success\": False,\n \"valid\": False,\n \"error\": str(e)\n }\n\ndef _handle_verify_error(self, error: TwilioRestException) -> dict:\n \"\"\"Handle Verify-specific errors.\"\"\"\n error_handlers = {\n 60200: \"Invalid phone number format\",\n 60203: \"Max send attempts reached for this number\",\n 60205: \"Service not found - check VERIFY_SID\",\n 60223: \"Failed to create verification - carrier rejected\",\n }\n\n return {\n \"success\": False,\n \"error_code\": error.code,\n \"error\": error_handlers.get(error.code, error.msg)\n }","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Usage Example - Signup Flow","type":"text"}]},{"type":"paragraph","content":[{"text":"verify = TwilioVerify()","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Step 1: User enters phone number","type":"text"}]},{"type":"paragraph","content":[{"text":"result = verify.send_verification(\"+14155551234\", VerifyChannel.SMS) if result[\"success\"]: print(\"Code sent! Check your phone.\")","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Step 2: User enters the code they received","type":"text"}]},{"type":"paragraph","content":[{"text":"code = \"123456\" # From user input check = verify.check_verification(\"+14155551234\", code)","type":"text"}]},{"type":"paragraph","content":[{"text":"if check[\"valid\"]: print(\"Phone verified! Create account.\") else: print(\"Invalid code. Try again.\")","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Best Practice: Offer voice fallback","type":"text"}]},{"type":"paragraph","content":[{"text":"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)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":" if result[\"success\"]:\n return result\n\n # If SMS failed, wait and try voice\n if channel == VerifyChannel.SMS:\n await asyncio.sleep(30)\n continue\n\nreturn {\"success\": False, \"error\": \"All verification attempts failed\"}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Anti_patterns","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Storing OTP codes in your database (Twilio handles this)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Not implementing rate limiting on your verify endpoint","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Using same-code retries (let Verify generate new codes)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No fallback channel when SMS fails","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"TwiML IVR Pattern","type":"text"}]},{"type":"paragraph","content":[{"text":"Build Interactive Voice Response (IVR) systems using TwiML. TwiML (Twilio Markup Language) is XML that tells Twilio what to do when receiving calls.","type":"text"}]},{"type":"paragraph","content":[{"text":"Core TwiML verbs:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\u003cSay>: Text-to-speech","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\u003cPlay>: Play audio file","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\u003cGather>: Collect keypad/speech input","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\u003cDial>: Connect to another number","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\u003cRecord>: Record caller's voice","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\u003cRedirect>: Move to another TwiML endpoint","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Key insight: Twilio makes HTTP request to your webhook, you return TwiML, Twilio executes it. Stateless, so use URL params or sessions.","type":"text"}]},{"type":"paragraph","content":[{"text":"When to use","type":"text","marks":[{"type":"strong"}]},{"text":": Phone menu systems (press 1 for sales...),Automated customer support,Appointment reminders with confirmation,Voicemail systems","type":"text"}]},{"type":"paragraph","content":[{"text":"from flask import Flask, request, Response from twilio.twiml.voice_response import VoiceResponse, Gather from twilio.request_validator import RequestValidator import os","type":"text"}]},{"type":"paragraph","content":[{"text":"app = Flask(","type":"text"},{"text":"name","type":"text","marks":[{"type":"underline"}]},{"text":")","type":"text"}]},{"type":"paragraph","content":[{"text":"def validate_twilio_request(f): \"\"\"Decorator to validate requests are from Twilio.\"\"\" def wrapper(*args, **kwargs): validator = RequestValidator(os.environ[\"TWILIO_AUTH_TOKEN\"])","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":" # Get request details\n url = request.url\n params = request.form.to_dict()\n signature = request.headers.get(\"X-Twilio-Signature\", \"\")\n\n if not validator.validate(url, params, signature):\n return \"Invalid request\", 403\n\n return f(*args, **kwargs)\nwrapper.__name__ = f.__name__\nreturn wrapper","type":"text"}]},{"type":"paragraph","content":[{"text":"@app.route(\"/voice/incoming\", methods=[\"POST\"]) @validate_twilio_request def incoming_call(): \"\"\"Handle incoming call with IVR menu.\"\"\" response = VoiceResponse()","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"# Gather digits with timeout\ngather = Gather(\n num_digits=1,\n action=\"/voice/menu-selection\",\n method=\"POST\",\n timeout=5\n)\ngather.say(\n \"Welcome to Acme Corp. \"\n \"Press 1 for sales. \"\n \"Press 2 for support. \"\n \"Press 3 to leave a message.\"\n)\nresponse.append(gather)\n\n# If no input, repeat\nresponse.redirect(\"/voice/incoming\")\n\nreturn Response(str(response), mimetype=\"text/xml\")","type":"text"}]},{"type":"paragraph","content":[{"text":"@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\", \"\")","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"if digit == \"1\":\n # Transfer to sales\n response.say(\"Connecting you to sales.\")\n response.dial(os.environ[\"SALES_PHONE\"])\n\nelif digit == \"2\":\n # Transfer to support\n response.say(\"Connecting you to support.\")\n response.dial(os.environ[\"SUPPORT_PHONE\"])\n\nelif digit == \"3\":\n # Voicemail\n response.say(\"Please leave a message after the beep.\")\n response.record(\n action=\"/voice/voicemail-saved\",\n max_length=120,\n transcribe=True,\n transcribe_callback=\"/voice/transcription\"\n )\n\nelse:\n response.say(\"Invalid selection.\")\n response.redirect(\"/voice/incoming\")\n\nreturn Response(str(response), mimetype=\"text/xml\")","type":"text"}]},{"type":"paragraph","content":[{"text":"@app.route(\"/voice/voicemail-saved\", methods=[\"POST\"]) @validate_twilio_request def voicemail_saved(): \"\"\"Handle saved voicemail.\"\"\" response = VoiceResponse()","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"recording_url = request.form.get(\"RecordingUrl\")\nrecording_sid = request.form.get(\"RecordingSid\")\n\n# Save to database, notify team, etc.\nprint(f\"Voicemail saved: {recording_url}\")\n\nresponse.say(\"Thank you. Goodbye.\")\nresponse.hangup()\n\nreturn Response(str(response), mimetype=\"text/xml\")","type":"text"}]},{"type":"paragraph","content":[{"text":"@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\")","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"# Save transcription, send to Slack, etc.\nprint(f\"Transcription: {transcription}\")\n\nreturn \"\", 200","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Outbound call example","type":"text"}]},{"type":"paragraph","content":[{"text":"from twilio.rest import Client","type":"text"}]},{"type":"paragraph","content":[{"text":"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\"] )","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"# TwiML Bin URL or your endpoint\ncall = client.calls.create(\n to=to,\n from_=os.environ[\"TWILIO_PHONE_NUMBER\"],\n url=\"https://your-app.com/voice/outbound-message\",\n status_callback=\"https://your-app.com/voice/status\"\n)\n\nreturn call.sid","type":"text"}]},{"type":"paragraph","content":[{"text":"if ","type":"text"},{"text":"name","type":"text","marks":[{"type":"underline"}]},{"text":" == \"","type":"text"},{"text":"main","type":"text","marks":[{"type":"underline"}]},{"text":"\": app.run(debug=True)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Anti_patterns","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Not validating X-Twilio-Signature (security risk)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Returning non-XML responses to Twilio","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Not handling timeout/no-input cases","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hardcoding phone numbers in TwiML","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"WhatsApp Business API Pattern","type":"text"}]},{"type":"paragraph","content":[{"text":"Send and receive WhatsApp messages via Twilio API. Uses the same Twilio Messages API as SMS with minor changes.","type":"text"}]},{"type":"paragraph","content":[{"text":"Key WhatsApp rules:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"24-hour session window: Can only reply within 24 hours of user message","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Template messages: Pre-approved templates for outside session window","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Opt-in required: Users must explicitly consent to receive messages","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rate limit: 80 MPS default (up to 400 with approval)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Character limits: Non-template 1024 chars, templates ~550 chars","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When to use","type":"text","marks":[{"type":"strong"}]},{"text":": Customer support with rich media,Order notifications with buttons,Marketing messages (with templates),Interactive flows (booking, surveys)","type":"text"}]},{"type":"paragraph","content":[{"text":"from twilio.rest import Client from twilio.base.exceptions import TwilioRestException import os from datetime import datetime, timedelta from typing import Optional","type":"text"}]},{"type":"paragraph","content":[{"text":"class TwilioWhatsApp: \"\"\" WhatsApp Business API via Twilio. Handles session windows and template messages. \"\"\"","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"def __init__(self):\n self.client = Client(\n os.environ[\"TWILIO_ACCOUNT_SID\"],\n os.environ[\"TWILIO_AUTH_TOKEN\"]\n )\n # WhatsApp number format: whatsapp:+14155551234\n self.from_number = os.environ[\"TWILIO_WHATSAPP_NUMBER\"]\n\ndef send_message(\n self,\n to: str,\n body: str,\n media_url: Optional[str] = None\n) -> dict:\n \"\"\"\n Send WhatsApp message within 24-hour session.\n\n Args:\n to: Recipient number (E.164, without whatsapp: prefix)\n body: Message text (max 1024 chars for non-template)\n media_url: Optional image/document URL\n\n Returns:\n Message result\n \"\"\"\n # Format for WhatsApp\n to_whatsapp = f\"whatsapp:{to}\"\n from_whatsapp = f\"whatsapp:{self.from_number}\"\n\n try:\n message_params = {\n \"to\": to_whatsapp,\n \"from_\": from_whatsapp,\n \"body\": body\n }\n\n if media_url:\n message_params[\"media_url\"] = [media_url]\n\n message = self.client.messages.create(**message_params)\n\n return {\n \"success\": True,\n \"message_sid\": message.sid,\n \"status\": message.status\n }\n\n except TwilioRestException as e:\n return self._handle_whatsapp_error(e)\n\ndef send_template_message(\n self,\n to: str,\n content_sid: str,\n content_variables: dict\n) -> dict:\n \"\"\"\n Send pre-approved template message.\n Use this for messages outside 24-hour window.\n\n Content templates must be approved by WhatsApp first.\n Create them in Twilio Console > Content Template Builder.\n \"\"\"\n to_whatsapp = f\"whatsapp:{to}\"\n from_whatsapp = f\"whatsapp:{self.from_number}\"\n\n try:\n message = self.client.messages.create(\n to=to_whatsapp,\n from_=from_whatsapp,\n content_sid=content_sid,\n content_variables=content_variables\n )\n\n return {\n \"success\": True,\n \"message_sid\": message.sid,\n \"template\": True\n }\n\n except TwilioRestException as e:\n return self._handle_whatsapp_error(e)\n\ndef _handle_whatsapp_error(self, error: TwilioRestException) -> dict:\n \"\"\"Handle WhatsApp-specific errors.\"\"\"\n error_handlers = {\n 63016: \"Outside 24-hour window. Use template message.\",\n 63018: \"Template not approved or doesn't exist.\",\n 63025: \"Too many template messages sent to this user.\",\n 63038: \"Rate limit exceeded for WhatsApp.\",\n }\n\n return {\n \"success\": False,\n \"error_code\": error.code,\n \"error\": error_handlers.get(error.code, error.msg)\n }","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Flask webhook for incoming WhatsApp messages","type":"text"}]},{"type":"paragraph","content":[{"text":"from flask import Flask, request","type":"text"}]},{"type":"paragraph","content":[{"text":"app = Flask(","type":"text"},{"text":"name","type":"text","marks":[{"type":"underline"}]},{"text":")","type":"text"}]},{"type":"paragraph","content":[{"text":"@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","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"# Track session start (24-hour window begins now)\nsession_start = datetime.now()\nsession_expires = session_start + timedelta(hours=24)\n\n# Store in database for session tracking\n# user_sessions[from_number] = session_expires\n\n# Process message and respond\nresponse = process_whatsapp_message(from_number, body, media_url)\n\n# Reply within session\nwhatsapp = TwilioWhatsApp()\nwhatsapp.send_message(from_number, response)\n\nreturn \"\", 200","type":"text"}]},{"type":"paragraph","content":[{"text":"def process_whatsapp_message(phone: str, text: str, media: str) -> str: \"\"\"Process incoming message and generate response.\"\"\" text_lower = text.lower()","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"if \"order status\" in text_lower:\n return \"Your order #1234 is out for delivery!\"\nelif \"support\" in text_lower:\n return \"A support agent will contact you shortly.\"\nelse:\n return \"Thanks for your message! Reply with 'order status' or 'support'.\"","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Send typing indicator (2025 feature)","type":"text"}]},{"type":"paragraph","content":[{"text":"def send_typing_indicator(to: str): \"\"\"Let user know you're typing.\"\"\" # Requires Senders API setup pass","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Anti_patterns","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sending non-template messages outside 24-hour window","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Not tracking session windows per user","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Exceeding 1024 char limit for session messages","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Not handling template rejection errors","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Webhook Handler Pattern","type":"text"}]},{"type":"paragraph","content":[{"text":"Handle Twilio webhooks for delivery status, incoming messages, and call events. Critical: always validate X-Twilio-Signature.","type":"text"}]},{"type":"paragraph","content":[{"text":"Twilio sends webhooks for:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Message status updates (queued → sent → delivered/failed)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Incoming SMS/WhatsApp messages","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Call events (initiated, ringing, answered, completed)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Recording/transcription ready","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When to use","type":"text","marks":[{"type":"strong"}]},{"text":": Tracking message delivery status,Receiving incoming messages,Call analytics and logging,Voicemail transcription processing","type":"text"}]},{"type":"paragraph","content":[{"text":"from flask import Flask, request, abort from twilio.request_validator import RequestValidator from functools import wraps import os import logging","type":"text"}]},{"type":"paragraph","content":[{"text":"app = Flask(","type":"text"},{"text":"name","type":"text","marks":[{"type":"underline"}]},{"text":") logger = logging.getLogger(","type":"text"},{"text":"name","type":"text","marks":[{"type":"underline"}]},{"text":")","type":"text"}]},{"type":"paragraph","content":[{"text":"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\"])","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":" # Build full URL (including query params)\n url = request.url\n\n # Get POST body as dict\n params = request.form.to_dict()\n\n # Get signature from header\n signature = request.headers.get(\"X-Twilio-Signature\", \"\")\n\n if not validator.validate(url, params, signature):\n logger.warning(f\"Invalid Twilio signature from {request.remote_addr}\")\n abort(403)\n\n return f(*args, **kwargs)\nreturn wrapper","type":"text"}]},{"type":"paragraph","content":[{"text":"@app.route(\"/webhooks/twilio/sms/status\", methods=[\"POST\"]) @validate_twilio_signature def sms_status_callback(): \"\"\" Handle SMS delivery status updates.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Status progression: queued → sending → sent → delivered\nOr: queued → sending → undelivered/failed\n\"\"\"\nmessage_sid = request.form.get(\"MessageSid\")\nstatus = request.form.get(\"MessageStatus\")\nerror_code = request.form.get(\"ErrorCode\")\nerror_message = request.form.get(\"ErrorMessage\")\n\nlogger.info(f\"SMS {message_sid}: {status}\")\n\nif status == \"delivered\":\n # Message successfully delivered\n update_message_status(message_sid, \"delivered\")\n\nelif status == \"undelivered\":\n # Carrier rejected or other failure\n logger.error(f\"SMS failed: {error_code} - {error_message}\")\n handle_failed_message(message_sid, error_code, error_message)\n\nelif status == \"failed\":\n # Twilio couldn't send\n logger.error(f\"SMS send failed: {error_code}\")\n handle_failed_message(message_sid, error_code, error_message)\n\nreturn \"\", 200","type":"text"}]},{"type":"paragraph","content":[{"text":"@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))","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"# Handle media attachments\nmedia_urls = []\nfor i in range(num_media):\n media_urls.append(request.form.get(f\"MediaUrl{i}\"))\n\n# Check for opt-out keywords\nif body.strip().upper() in [\"STOP\", \"UNSUBSCRIBE\", \"CANCEL\"]:\n handle_opt_out(from_number)\n return \"\", 200\n\n# Check for opt-in keywords\nif body.strip().upper() in [\"START\", \"SUBSCRIBE\"]:\n handle_opt_in(from_number)\n return \"\", 200\n\n# Process message\nprocess_incoming_sms(from_number, body, media_urls)\n\nreturn \"\", 200","type":"text"}]},{"type":"paragraph","content":[{"text":"@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\")","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"# Call statuses: initiated, ringing, in-progress, completed, busy, no-answer, canceled, failed\n\nlogger.info(f\"Call {call_sid}: {status} ({duration}s)\")\n\nif status == \"completed\":\n # Call ended normally\n log_call_completion(call_sid, duration)\n\nelif status in [\"busy\", \"no-answer\", \"canceled\", \"failed\"]:\n # Call didn't connect\n handle_failed_call(call_sid, status)\n\nreturn \"\", 200","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Helper functions","type":"text"}]},{"type":"paragraph","content":[{"text":"def update_message_status(message_sid: str, status: str): \"\"\"Update message status in database.\"\"\" pass","type":"text"}]},{"type":"paragraph","content":[{"text":"def handle_failed_message(message_sid: str, error_code: str, error_msg: str): \"\"\"Handle failed message delivery.\"\"\" # Notify team, retry logic, etc. pass","type":"text"}]},{"type":"paragraph","content":[{"text":"def handle_opt_out(phone: str): \"\"\"Handle user opting out of messages.\"\"\" # Mark user as opted out in database # IMPORTANT: Must respect this! pass","type":"text"}]},{"type":"paragraph","content":[{"text":"def handle_opt_in(phone: str): \"\"\"Handle user opting back in.\"\"\" pass","type":"text"}]},{"type":"paragraph","content":[{"text":"def process_incoming_sms(from_phone: str, body: str, media: list): \"\"\"Process incoming SMS message.\"\"\" pass","type":"text"}]},{"type":"paragraph","content":[{"text":"def log_call_completion(call_sid: str, duration: str): \"\"\"Log completed call.\"\"\" pass","type":"text"}]},{"type":"paragraph","content":[{"text":"def handle_failed_call(call_sid: str, status: str): \"\"\"Handle call that didn't connect.\"\"\" pass","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Anti_patterns","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Not validating X-Twilio-Signature","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Exposing webhook URLs without authentication","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Not handling opt-out keywords (STOP)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Blocking webhook response (should be fast)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Rate Limit and Retry Pattern","type":"text"}]},{"type":"paragraph","content":[{"text":"Handle Twilio rate limits and implement proper retry logic.","type":"text"}]},{"type":"paragraph","content":[{"text":"Default limits:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SMS: 80 messages per second (MPS)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Voice: Varies by number type and region","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"API calls: 100 requests per second","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Error codes:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"20429: Voice API rate limit","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"30429: Messaging API rate limit","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When to use","type":"text","marks":[{"type":"strong"}]},{"text":": High-volume messaging applications,Bulk SMS campaigns,Automated calling systems","type":"text"}]},{"type":"paragraph","content":[{"text":"import time import random from functools import wraps from twilio.base.exceptions import TwilioRestException import logging","type":"text"}]},{"type":"paragraph","content":[{"text":"logger = logging.getLogger(","type":"text"},{"text":"name","type":"text","marks":[{"type":"underline"}]},{"text":")","type":"text"}]},{"type":"paragraph","content":[{"text":"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.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Uses jitter to prevent thundering herd.\n\"\"\"\ndef decorator(func):\n @wraps(func)\n def wrapper(*args, **kwargs):\n last_exception = None\n\n for attempt in range(max_retries + 1):\n try:\n return func(*args, **kwargs)\n\n except TwilioRestException as e:\n last_exception = e\n\n # Only retry on rate limit errors\n if e.code not in rate_limit_codes:\n raise\n\n if attempt == max_retries:\n logger.error(f\"Max retries exceeded: {e}\")\n raise\n\n # Calculate delay with jitter\n delay = min(\n base_delay * (2 ** attempt) + random.uniform(0, 1),\n max_delay\n )\n\n logger.warning(\n f\"Rate limited (attempt {attempt + 1}/{max_retries}). \"\n f\"Retrying in {delay:.1f}s\"\n )\n time.sleep(delay)\n\n raise last_exception\n\n return wrapper\nreturn decorator","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Usage","type":"text"}]},{"type":"paragraph","content":[{"text":"from twilio.rest import Client","type":"text"}]},{"type":"paragraph","content":[{"text":"client = Client(account_sid, auth_token)","type":"text"}]},{"type":"paragraph","content":[{"text":"@exponential_backoff_retry(max_retries=5) def send_sms(to: str, body: str): return client.messages.create( to=to, from_=from_number, body=body )","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Bulk sending with rate limiting","type":"text"}]},{"type":"paragraph","content":[{"text":"import asyncio from asyncio import Semaphore","type":"text"}]},{"type":"paragraph","content":[{"text":"class RateLimitedSender: \"\"\" Send messages with built-in rate limiting. Stays under Twilio's 80 MPS limit. \"\"\"","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"def __init__(self, client, from_number: str, mps: int = 50):\n self.client = client\n self.from_number = from_number\n self.mps = mps\n self.semaphore = Semaphore(mps)\n\nasync def send_bulk(self, messages: list[dict]) -> list[dict]:\n \"\"\"\n Send messages with rate limiting.\n\n Args:\n messages: List of {\"to\": \"+1...\", \"body\": \"...\"}\n\n Returns:\n Results for each message\n \"\"\"\n tasks = [\n self._send_with_limit(msg[\"to\"], msg[\"body\"])\n for msg in messages\n ]\n\n return await asyncio.gather(*tasks, return_exceptions=True)\n\nasync def _send_with_limit(self, to: str, body: str):\n \"\"\"Send single message with semaphore-based rate limit.\"\"\"\n async with self.semaphore:\n try:\n # Use sync client in thread pool\n loop = asyncio.get_event_loop()\n result = await loop.run_in_executor(\n None,\n lambda: self.client.messages.create(\n to=to,\n from_=self.from_number,\n body=body\n )\n )\n return {\"success\": True, \"sid\": result.sid, \"to\": to}\n\n except TwilioRestException as e:\n return {\"success\": False, \"error\": str(e), \"to\": to}\n\n finally:\n # Delay to maintain rate limit\n await asyncio.sleep(1 / self.mps)","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Usage","type":"text"}]},{"type":"paragraph","content":[{"text":"async def send_campaign(): sender = RateLimitedSender(client, from_number, mps=50)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"messages = [\n {\"to\": \"+14155551234\", \"body\": \"Hello!\"},\n {\"to\": \"+14155555678\", \"body\": \"Hello!\"},\n # ... thousands of messages\n]\n\nresults = await sender.send_bulk(messages)\n\nsuccessful = sum(1 for r in results if r.get(\"success\"))\nprint(f\"Sent {successful}/{len(messages)} messages\")","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Anti_patterns","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Retrying immediately without backoff","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No jitter causing thundering herd","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Retrying non-rate-limit errors","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Exceeding Twilio's MPS limit","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Sharp Edges","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Sending to Users Who Opted Out (Error 21610)","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: HIGH","type":"text"}]},{"type":"paragraph","content":[{"text":"Situation: Sending SMS to a phone number","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms: Message fails with error code 21610. Twilio rejects the message. User never receives the SMS. Same number worked before.","type":"text"}]},{"type":"paragraph","content":[{"text":"Why this breaks: The recipient replied \"STOP\" (or UNSUBSCRIBE, CANCEL, etc.) to a previous message from your number. Twilio automatically honors opt-outs and blocks further messages to that number from your account.","type":"text"}]},{"type":"paragraph","content":[{"text":"This is legally required for US messaging (TCPA, CTIA guidelines). You cannot override this - the user must reply \"START\" to opt back in.","type":"text"}]},{"type":"paragraph","content":[{"text":"Recommended fix:","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Track opt-out status in your database","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# In your webhook handler\[email protected](\"/webhooks/sms/incoming\", methods=[\"POST\"])\ndef incoming_sms():\n from_number = request.form.get(\"From\")\n body = request.form.get(\"Body\", \"\").strip().upper()\n\n # Standard opt-out keywords\n if body in [\"STOP\", \"UNSUBSCRIBE\", \"CANCEL\", \"END\", \"QUIT\"]:\n mark_user_opted_out(from_number)\n return \"\", 200\n\n # Standard opt-in keywords\n if body in [\"START\", \"SUBSCRIBE\", \"YES\", \"UNSTOP\"]:\n mark_user_opted_in(from_number)\n return \"\", 200\n\n # Process other messages...\n\n# Before sending\ndef send_sms_safe(to: str, body: str):\n if is_user_opted_out(to):\n return {\"success\": False, \"error\": \"User has opted out\"}\n\n try:\n return send_sms(to, body)\n except TwilioRestException as e:\n if e.code == 21610:\n # Update database - they opted out via carrier\n mark_user_opted_out(to)\n raise","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Include opt-out instructions","type":"text"}]},{"type":"paragraph","content":[{"text":"Add \"Reply STOP to unsubscribe\" to marketing messages.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Phone Unreachable But Valid (Error 30003)","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: MEDIUM","type":"text"}]},{"type":"paragraph","content":[{"text":"Situation: Sending SMS to a mobile number","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms: Message fails with error 30003. Number was valid and worked before. Intermittent - sometimes works, sometimes fails.","type":"text"}]},{"type":"paragraph","content":[{"text":"Why this breaks: Error 30003 means \"Unreachable destination handset.\" The phone exists but can't receive messages right now. Common causes:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Phone powered off","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Airplane mode","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Out of signal range","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Carrier network issues","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Phone storage full","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Unlike 30006 (permanent unreachable), 30003 is usually temporary.","type":"text"}]},{"type":"paragraph","content":[{"text":"Recommended fix:","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Implement retry logic for transient failures","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"TRANSIENT_ERRORS = [30003, 30008, 30009] # Retriable errors\n\nasync def send_with_retry(to: str, body: str, max_retries: int = 3):\n for attempt in range(max_retries):\n result = send_sms(to, body)\n\n if result[\"success\"]:\n return result\n\n if result.get(\"error_code\") not in TRANSIENT_ERRORS:\n # Don't retry permanent failures\n return result\n\n # Exponential backoff: 5min, 15min, 45min\n delay = 300 * (3 ** attempt)\n await asyncio.sleep(delay)\n\n return {\"success\": False, \"error\": \"Max retries exceeded\"}","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Provide fallback channel","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"async def notify_user(user, message):\n # Try SMS first\n result = await send_sms(user.phone, message)\n\n if result.get(\"error_code\") == 30003:\n # Phone unreachable - try email\n await send_email(user.email, message)\n return {\"channel\": \"email\", \"status\": \"sent\"}\n\n return {\"channel\": \"sms\", \"status\": result[\"status\"]}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Messages Blocked by Carrier Filtering","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: HIGH","type":"text"}]},{"type":"paragraph","content":[{"text":"Situation: Sending SMS to US phone numbers","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms: Messages show as \"sent\" but never \"delivered.\" No error from Twilio. Users say they never received the message. Pattern in specific carriers or message content.","type":"text"}]},{"type":"paragraph","content":[{"text":"Why this breaks: US carriers (Verizon, AT&T, T-Mobile) aggressively filter SMS for spam. Your message might be blocked if:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Contains URLs (especially short URLs or unknown domains)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Looks like phishing (urgent, account, verify, click now)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"High volume from same number","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Not using registered A2P 10DLC","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Low sender reputation","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Carriers don't tell Twilio why messages are filtered - they just silently drop them.","type":"text"}]},{"type":"paragraph","content":[{"text":"Recommended fix:","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Register for A2P 10DLC (US requirement)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"1. Go to Twilio Console > Messaging > Trust Hub\n2. Register your business brand\n3. Create a messaging campaign (describes use case)\n4. Wait for approval (can take days)\n5. Associate phone numbers with campaign","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Message content best practices","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"def sanitize_message(text: str) -> str:\n \"\"\"Make message less likely to be filtered.\"\"\"\n # Avoid URL shorteners - use full domain\n # Avoid spam trigger words\n # Keep it conversational, not promotional\n\n # Example: Instead of this\n bad = \"URGENT: Verify your account now! Click: bit.ly/abc\"\n\n # Do this\n good = \"Hi! Your order #1234 is ready. Questions? Reply here.\"\n\n return text\n\n# Use toll-free or short code for high volume\n# 10DLC is for \u003c10K msg/day\n# Toll-free: up to 10K msg/day\n# Short code: 100K+ msg/day","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Monitor delivery rates","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"def track_delivery_rate():\n sent = get_messages_with_status(\"sent\")\n delivered = get_messages_with_status(\"delivered\")\n\n rate = len(delivered) / len(sent) * 100\n\n if rate \u003c 95:\n alert_team(f\"Delivery rate dropped to {rate}%\")","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Not Validating Webhook Signatures","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: CRITICAL","type":"text"}]},{"type":"paragraph","content":[{"text":"Situation: Receiving Twilio webhook callbacks","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms: Attackers send fake webhooks to your endpoint. Fraudulent transactions processed. Spoofed incoming messages trigger actions.","type":"text"}]},{"type":"paragraph","content":[{"text":"Why this breaks: Twilio signs all webhook requests with X-Twilio-Signature header. If you don't validate this, anyone who knows your webhook URL can send fake requests pretending to be Twilio.","type":"text"}]},{"type":"paragraph","content":[{"text":"This can lead to:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fake message delivery confirmations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Spoofed incoming messages","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fraudulent verification approvals","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Recommended fix:","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"ALWAYS validate the signature","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from twilio.request_validator import RequestValidator\nfrom flask import Flask, request, abort\nfrom functools import wraps\nimport os\n\ndef require_twilio_signature(f):\n \"\"\"Decorator to validate Twilio webhook requests.\"\"\"\n @wraps(f)\n def wrapper(*args, **kwargs):\n validator = RequestValidator(os.environ[\"TWILIO_AUTH_TOKEN\"])\n\n # Full URL including query string\n url = request.url\n\n # POST body as dict\n params = request.form.to_dict()\n\n # Signature header\n signature = request.headers.get(\"X-Twilio-Signature\", \"\")\n\n if not validator.validate(url, params, signature):\n abort(403)\n\n return f(*args, **kwargs)\n return wrapper\n\[email protected](\"/webhooks/twilio\", methods=[\"POST\"])\n@require_twilio_signature # ALWAYS use this\ndef twilio_webhook():\n # Safe to process\n pass","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common validation gotchas","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# URL must match EXACTLY what Twilio called\n# If behind proxy, you might need:\nurl = request.headers.get(\"X-Forwarded-Proto\", \"http\") + \"://\" + \\\n request.headers.get(\"X-Forwarded-Host\", request.host) + \\\n request.path\n\n# If using ngrok, URL changes each restart\n# Use consistent URL in production","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"WhatsApp Message Outside 24-Hour Window (Error 63016)","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: HIGH","type":"text"}]},{"type":"paragraph","content":[{"text":"Situation: Sending WhatsApp message to a user","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms: Message fails with error 63016. \"Message is outside the allowed window.\" Template messages work, but regular messages fail.","type":"text"}]},{"type":"paragraph","content":[{"text":"Why this breaks: WhatsApp has strict rules about unsolicited messages:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Users must message you first","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"You can only reply within 24 hours of their last message","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After 24 hours, you must use pre-approved template messages","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"This prevents spam and maintains WhatsApp's trust as a platform.","type":"text"}]},{"type":"paragraph","content":[{"text":"Recommended fix:","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Track session windows per user","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from datetime import datetime, timedelta\n\nclass WhatsAppSession:\n def __init__(self, redis_client):\n self.redis = redis_client\n self.window_hours = 24\n\n def start_session(self, phone: str):\n \"\"\"Start/refresh 24-hour session on incoming message.\"\"\"\n key = f\"wa_session:{phone}\"\n expires = datetime.now() + timedelta(hours=self.window_hours)\n self.redis.set(key, expires.isoformat(), ex=self.window_hours * 3600)\n\n def can_send_freeform(self, phone: str) -> bool:\n \"\"\"Check if we can send non-template message.\"\"\"\n key = f\"wa_session:{phone}\"\n expires_str = self.redis.get(key)\n\n if not expires_str:\n return False\n\n expires = datetime.fromisoformat(expires_str)\n return datetime.now() \u003c expires\n\n def send_message(self, phone: str, body: str, template_sid: str = None):\n \"\"\"Send message, using template if outside window.\"\"\"\n if self.can_send_freeform(phone):\n return send_whatsapp_message(phone, body)\n elif template_sid:\n return send_whatsapp_template(phone, template_sid)\n else:\n return {\n \"success\": False,\n \"error\": \"Outside session window, template required\"\n }","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Incoming message webhook","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"@app.route(\"/webhooks/whatsapp\", methods=[\"POST\"])\ndef whatsapp_incoming():\n from_phone = request.form.get(\"From\").replace(\"whatsapp:\", \"\")\n\n # Start/refresh session\n session.start_session(from_phone)\n\n # Process message...","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Create approved templates for common messages","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"1. Twilio Console > Content Template Builder\n2. Create template with {{1}} placeholders\n3. Submit for WhatsApp approval (takes 24-48 hours)\n4. Use content_sid to send","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Exposed Account SID or Auth Token","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: CRITICAL","type":"text"}]},{"type":"paragraph","content":[{"text":"Situation: Deploying Twilio integration","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms: Unauthorized charges on Twilio account. Messages sent you didn't send. Phone numbers purchased without authorization.","type":"text"}]},{"type":"paragraph","content":[{"text":"Why this breaks: If attackers get your Account SID + Auth Token, they have FULL access to your Twilio account. They can:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Send messages (charging your account)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Buy phone numbers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Access call recordings","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Modify your configuration","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Common exposure points:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hardcoded in source code (pushed to GitHub)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"In client-side JavaScript","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"In Docker images","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"In logs","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Recommended fix:","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Never hardcode credentials","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# BAD - never do this\nclient = Client(\"AC1234...\", \"abc123...\")\n\n# GOOD - environment variables\nclient = Client(\n os.environ[\"TWILIO_ACCOUNT_SID\"],\n os.environ[\"TWILIO_AUTH_TOKEN\"]\n)\n\n# GOOD - secrets manager\nfrom aws_secretsmanager import get_secret\ncreds = get_secret(\"twilio-credentials\")\nclient = Client(creds[\"sid\"], creds[\"token\"])","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Use API Key instead of Auth Token","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# Auth Token has full account access\n# API Keys can be scoped and revoked\n\n# Create API Key in Twilio Console\nclient = Client(\n os.environ[\"TWILIO_API_KEY_SID\"],\n os.environ[\"TWILIO_API_KEY_SECRET\"],\n os.environ[\"TWILIO_ACCOUNT_SID\"]\n)\n\n# If compromised, revoke just that key","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rotate tokens immediately if exposed","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"1. Twilio Console > Account > API credentials\n2. Rotate Auth Token\n3. Update all deployments with new token\n4. Review account activity for unauthorized use","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Verify Rate Limit Exceeded (Error 60203)","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: MEDIUM","type":"text"}]},{"type":"paragraph","content":[{"text":"Situation: Sending verification codes","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptoms: Verification request fails with error 60203. \"Max send attempts reached for this phone number.\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Why this breaks: Twilio Verify has built-in rate limits to prevent abuse:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"5 verification attempts per phone number per service per 10 minutes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Helps prevent SMS pumping fraud","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Protects against brute-force attacks","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"If users legitimately need more attempts, you may have UX issues.","type":"text"}]},{"type":"paragraph","content":[{"text":"Recommended fix:","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Implement application-level rate limiting too","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from datetime import datetime, timedelta\nimport redis\n\nclass VerifyRateLimiter:\n def __init__(self, redis_client):\n self.redis = redis_client\n # Stricter than Twilio's limit\n self.max_attempts = 3\n self.window_minutes = 10\n\n def can_request(self, phone: str) -> bool:\n key = f\"verify_rate:{phone}\"\n attempts = self.redis.get(key)\n\n if attempts and int(attempts) >= self.max_attempts:\n return False\n\n return True\n\n def record_attempt(self, phone: str):\n key = f\"verify_rate:{phone}\"\n pipe = self.redis.pipeline()\n pipe.incr(key)\n pipe.expire(key, self.window_minutes * 60)\n pipe.execute()\n\n def get_wait_time(self, phone: str) -> int:\n \"\"\"Return seconds until user can request again.\"\"\"\n key = f\"verify_rate:{phone}\"\n ttl = self.redis.ttl(key)\n return max(0, ttl)\n\n# Usage\nlimiter = VerifyRateLimiter(redis_client)\n\[email protected](\"/verify/send\", methods=[\"POST\"])\ndef send_verification():\n phone = request.json[\"phone\"]\n\n if not limiter.can_request(phone):\n wait = limiter.get_wait_time(phone)\n return {\n \"error\": f\"Too many attempts. Try again in {wait} seconds.\"\n }, 429\n\n result = twilio_verify.send_verification(phone)\n\n if result[\"success\"]:\n limiter.record_attempt(phone)\n\n return result","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Provide clear user feedback","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# Show remaining attempts\n# Show countdown timer\n# Offer alternative (voice call, email)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Validation Checks","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Hardcoded Twilio Credentials","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: ERROR","type":"text"}]},{"type":"paragraph","content":[{"text":"Twilio credentials must never be hardcoded","type":"text"}]},{"type":"paragraph","content":[{"text":"Message: Hardcoded Twilio SID detected. Use environment variables.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Auth Token in Source Code","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: ERROR","type":"text"}]},{"type":"paragraph","content":[{"text":"Auth tokens should be in environment variables","type":"text"}]},{"type":"paragraph","content":[{"text":"Message: Hardcoded auth token. Use os.environ['TWILIO_AUTH_TOKEN'].","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Webhook Without Signature Validation","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: ERROR","type":"text"}]},{"type":"paragraph","content":[{"text":"Twilio webhooks must validate X-Twilio-Signature","type":"text"}]},{"type":"paragraph","content":[{"text":"Message: Webhook without signature validation. Add RequestValidator check.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Twilio Credentials in Client-Side Code","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: ERROR","type":"text"}]},{"type":"paragraph","content":[{"text":"Never expose Twilio credentials to browsers","type":"text"}]},{"type":"paragraph","content":[{"text":"Message: Twilio credentials exposed client-side. Only use server-side.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"No E.164 Phone Number Validation","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: WARNING","type":"text"}]},{"type":"paragraph","content":[{"text":"Phone numbers should be validated before sending","type":"text"}]},{"type":"paragraph","content":[{"text":"Message: Sending to phone without E.164 validation.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Hardcoded Phone Numbers","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: WARNING","type":"text"}]},{"type":"paragraph","content":[{"text":"Phone numbers should come from config or database","type":"text"}]},{"type":"paragraph","content":[{"text":"Message: Hardcoded phone number. Use config or environment variable.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"No Twilio Exception Handling","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: WARNING","type":"text"}]},{"type":"paragraph","content":[{"text":"Twilio calls should handle TwilioRestException","type":"text"}]},{"type":"paragraph","content":[{"text":"Message: Twilio API call without error handling. Catch TwilioRestException.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Not Handling Specific Error Codes","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: INFO","type":"text"}]},{"type":"paragraph","content":[{"text":"Handle common Twilio error codes specifically","type":"text"}]},{"type":"paragraph","content":[{"text":"Message: Consider handling specific error codes (21610, 30003, etc.).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"No Opt-Out Keyword Handling","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: WARNING","type":"text"}]},{"type":"paragraph","content":[{"text":"SMS systems must handle STOP/UNSUBSCRIBE keywords","type":"text"}]},{"type":"paragraph","content":[{"text":"Message: No opt-out handling. Check for STOP/UNSUBSCRIBE keywords.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Not Checking Opt-Out Before Sending","type":"text"}]},{"type":"paragraph","content":[{"text":"Severity: WARNING","type":"text"}]},{"type":"paragraph","content":[{"text":"Check if user has opted out before sending SMS","type":"text"}]},{"type":"paragraph","content":[{"text":"Message: Consider checking opt-out status before sending.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Collaboration","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Delegation Triggers","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"user needs AI voice assistant -> voice-agents (Twilio provides telephony, voice-agents skill for AI conversation)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"user needs Slack notifications -> slack-bot-builder (Integrate SMS alerts with Slack notifications)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"user needs full auth system -> auth-specialist (Twilio Verify is one component of broader auth)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"user needs workflow automation -> workflow-automation (Trigger SMS/calls from automated workflows)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"user needs high-volume messaging -> devops (Scale webhooks, monitor delivery rates)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Use","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User mentions or implies: twilio","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User mentions or implies: send SMS","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User mentions or implies: text message","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User mentions or implies: voice call","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User mentions or implies: phone verification","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User mentions or implies: 2FA SMS","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User mentions or implies: WhatsApp API","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User mentions or implies: programmable messaging","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User mentions or implies: IVR system","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User mentions or implies: TwiML","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User mentions or implies: phone number verification","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Limitations","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use this skill only when the task clearly matches the scope described above.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do not treat the output as a substitute for environment-specific validation, testing, or expert review.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"twilio-communications","risk":"unknown","author":"@skillopedia","source":{"stars":39376,"repo_name":"antigravity-awesome-skills","origin_url":"https://github.com/sickn33/antigravity-awesome-skills/blob/HEAD/skills/twilio-communications/SKILL.md","repo_owner":"sickn33","body_sha256":"4dc0050cafe52cedd721816a407b54b6041809972c612f442015281824b08d70","cluster_key":"fe11f2b38880e7c531f8fb320567b4fa62bc03eccc4e98d6fe26741c9657f652","clean_bundle":{"format":"clean-skill-bundle-v1","source":"sickn33/antigravity-awesome-skills/skills/twilio-communications/SKILL.md","bundle_sha256":"b4e911ed9ec0ab8a8877498e0ec1f1deb69bca744b930755a7eeb6433ea4f085","attachment_count":0,"text_attachments":0,"binary_attachments":0},"cluster_size":4,"skill_md_path":"skills/twilio-communications/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":3},"version":"v1","category":"web-development","date_added":"2026-02-27T00:00:00.000Z","import_tag":"clean-skills-v1","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."}},"renderedAt":1782988118856}

Twilio Communications 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. Patterns SMS Sending Pattern 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 mess…