feat(voice): outbound Calls API passthrough, call events & AMD#78
Draft
ryanrouleau wants to merge 8 commits into
Draft
feat(voice): outbound Calls API passthrough, call events & AMD#78ryanrouleau wants to merge 8 commits into
ryanrouleau wants to merge 8 commits into
Conversation
Enable answering machine detection and call disposition reporting for outbound ConversationRelay calls: hang up on voicemail instead of monologuing at a machine, and observe which calls went unanswered / busy / failed (e.g. for outbound reconnect campaigns with retry logic). Three seams, matched to the shape of each surface: - call_options passthrough on InitiateVoiceConversationOptions: forwarded verbatim to calls.create (AMD, status_callback, record, timeout, SIP...). to/from/twiml are guarded. Callback URLs auto-wire from voice_public_domain + voice_call_event_path via setdefault, so an explicit URL always wins (passthrough preserved). status_callback wires whenever the domain is set; AMD/recording callbacks only when the dev opts into the feature. - on_call_event / handle_call_event + neutral CallEvent model: Twilio's three independent callback URLs (status, AMD, recording) all point at one route that TACFastAPIServer registers; handle_call_event classifies by payload fields and fires one handler discriminated by event.kind. - end_call(call_sid): hangs up via calls.update (works on CallSid alone, any mode) and best-effort tears down the CR session via a CallSid->conv_id map. Identifier plumbing so the surfaces correlate: CallSid is the public key everywhere. ConversationSession.call_sid is now populated on the voice channel (equals conversation_id in relay-only mode, the Twilio SID in orchestrator mode), matching CallEvent.call_sid and the outbound result. New config: voice_call_event_path / TWILIO_VOICE_CALL_EVENT_PATH. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ryanrouleau
force-pushed
the
feat/outbound-amd-call-events
branch
from
July 6, 2026 23:55
e33a49e to
e0c117c
Compare
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… fields Verified against Twilio's call-resource docs: - A completed statusCallback also posts RecordingSid/RecordingUrl, so classify recording on RecordingStatus (posted only by the recording callback) rather than on the presence of a recording SID — otherwise a completed call event with a recording would misclassify as kind="recording". - Surface the commonly-needed status/recording fields as typed attributes: call_duration, sip_response_code, recording_duration. (Everything remains available via event.raw.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the unified CallEvent/on_call_event surface with one typed event and handler per Twilio callback, matching the shape of the Calls API: - CallStatusEvent / on_status / handle_status_event - AmdEvent / on_amd / handle_amd_event - RecordingEvent / on_recording / handle_recording_event Each handler is independently optional and its event carries only its own fields (no flat 11-optional model). TACFastAPIServer serves one route per callback under voice_call_event_path (/status, /amd, /recording), so the route the webhook arrives on identifies the event — no ?kind= query tag and no payload-field guessing. The base path is trailing-slash normalized. The Voice API is stable, so mirroring its three callbacks 1:1 is simpler and more discoverable than a discriminated union: self-wirers see named routes and map them to Twilio's three callback params directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"status" is overloaded in the voice model: ConversationRelayCallbackPayload already carries both CallStatus and SessionStatus (the session-ended callback). Name the Calls-API disposition handler on_call_status (matching its CallStatusEvent type and Twilio's CallStatus field) so it doesn't read as the ConversationRelay session callback. on_amd/on_recording stay terse — no such collision. Also renames handle_status_event -> handle_call_status_event. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cut the internal "seam" framing and the comments that restated the code; keep only the comments that flag Twilio quirks (mixed str/bool call_options types, async_amd="true" required for the AMD callback). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ryanrishi
reviewed
Jul 14, 2026
Comment on lines
+69
to
+70
| "async_amd": "true", # required for the AMD callback (sync AMD won't fire it) | ||
| "record": True, |
Member
There was a problem hiding this comment.
Does using True instead of "true" work?
Suggested change
| "async_amd": "true", # required for the AMD callback (sync AMD won't fire it) | |
| "record": True, | |
| "async_amd": True, # required for the AMD callback (sync AMD won't fire it) | |
| "record": True, |
Comment on lines
+52
to
+59
| Twilio types these params inconsistently across its docs (``async_amd`` as | ||
| the string ``"true"``, ``record`` as a bool), and call_options is a free-form | ||
| passthrough, so accept both: real booleans and the string ``"true"`` (any | ||
| case). Everything else — ``False``, ``"false"``, ``None``, absent — is off. | ||
| """ | ||
| if isinstance(value, str): | ||
| return value.strip().lower() == "true" | ||
| return bool(value) |
Member
There was a problem hiding this comment.
Oh I see, is having both True and "true" in the example to demonstrate that you can pass either?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Enables AMD and recording on outbound ConversationRelay calls, and reports call disposition (unanswered / busy / failed) — for outbound chase/retry campaigns that need to hang up on voicemail and know which calls went unreached.
Three seams:
call_optionspassthrough —InitiateVoiceConversationOptions.call_optionsforwards verbatim tocalls.create(machine_detection,async_amd,record,timeout, SIP auth, …). Twilio validates it;to/from/twimlare reserved.on_status/on_amd/on_recording, each with its own typed event (CallStatusEvent/AmdEvent/RecordingEvent) and each independently optional.TACFastAPIServerserves one route per callback undervoice_call_event_path(/status,/amd,/recording), so the route identifies the event — no payload guessing.end_call(call_sid)— hangs up (works onCallSidalone) and best-effort tears down the ConversationRelay session; call it fromon_amdto drop voicemail.Callback URLs auto-wire from
voice_public_domain+voice_call_event_path:statuswhenever the domain is set,amd/recordingonly when the caller opts into that feature (async_amd="true"/recordtruthy). An explicit URL incall_optionsalways wins.ConversationSession.call_sidis populated so events correlate withon_message_ready.New config:
voice_call_event_path/TWILIO_VOICE_CALL_EVENT_PATH.Type of Change
Checklist
SDK Parity
This is the Python SDK. The same seams should land in the TypeScript SDK.