Webhook endpoint that Twilio calls when a voice call connects. Handles both incoming calls (whitelist them) and outgoing calls (speak message). Returns TwiML instructions. Validates Twilio signature to prevent spoofing attacks.
(
request: Request,
form_data: dict = Depends(validate_twilio_request)
)
| 832 | |
| 833 | @messaging_router.post("/webhooks/voice-callback", tags=["Webhooks"]) |
| 834 | async def voice_callback( |
| 835 | request: Request, |
| 836 | form_data: dict = Depends(validate_twilio_request) |
| 837 | ): |
| 838 | """ |
| 839 | Webhook endpoint that Twilio calls when a voice call connects. |
| 840 | Handles both incoming calls (whitelist them) and outgoing calls (speak message). |
| 841 | Returns TwiML instructions. |
| 842 | Validates Twilio signature to prevent spoofing attacks. |
| 843 | """ |
| 844 | try: |
| 845 | # Extract call info (form_data already validated and parsed) |
| 846 | call_sid = form_data.get("CallSid", "") |
| 847 | call_status = form_data.get("CallStatus", "") |
| 848 | direction = form_data.get("Direction", "") |
| 849 | from_number = form_data.get("From", "") |
| 850 | |
| 851 | logger.info(f"Voice callback received: SID={call_sid}, Status={call_status}, Direction={direction}") |
| 852 | |
| 853 | # Create TwiML response |
| 854 | response = VoiceResponse() |
| 855 | |
| 856 | if direction == "inbound": |
| 857 | # Someone called YOUR number - whitelist them for phone (SMS/Voice) |
| 858 | await add_to_whitelist(from_number, channel="phone") |
| 859 | logger.info(f"Auto-whitelisted caller {from_number} via incoming call") |
| 860 | |
| 861 | response.say( |
| 862 | "Your number is now whitelisted for 24 hours across SMS, WhatsApp, and voice calls. You can now receive messages from Observer AI.", |
| 863 | voice='alice', |
| 864 | language='en-US' |
| 865 | ) |
| 866 | else: |
| 867 | # Outgoing call - speak the message from AI |
| 868 | message_text = pending_voice_calls.get(call_sid, "Alert from Observer AI") |
| 869 | response.say(message_text, voice='alice', language='en-US') |
| 870 | |
| 871 | # Clean up the stored message |
| 872 | if call_sid in pending_voice_calls: |
| 873 | del pending_voice_calls[call_sid] |
| 874 | |
| 875 | # Return TwiML as XML |
| 876 | return FastAPIResponse(content=str(response), media_type="application/xml") |
| 877 | |
| 878 | except Exception as e: |
| 879 | logger.error(f"Error processing voice callback: {str(e)}") |
| 880 | # Return a fallback TwiML response |
| 881 | response = VoiceResponse() |
| 882 | response.say("An error occurred.", voice='alice') |
| 883 | return FastAPIResponse(content=str(response), media_type="application/xml") |
| 884 | |
| 885 | @messaging_router.post("/webhooks/voice-status", tags=["Webhooks"]) |
| 886 | async def voice_status_callback( |
nothing calls this directly
no test coverage detected