Docs / FreeSWITCH AMD integration

FreeSWITCH AMD Integration Guide

For teams already running FreeSWITCH who want to wire AMDY's AI detection into their existing call flow, rather than a fresh install. If you have not installed the AMDY fork module and dialplan snippet yet, start with the FreeSWITCH install guide. This page covers the integration layer: choosing between mod_avmd and mod_audio_fork, dialplan XML wiring, Event Socket (ESL) hooks for external controllers, and codec-level gotchas.

Two ways to connect FreeSWITCH. This guide covers wiring the mod_audio_fork integration into an existing call flow. We also have our own native module, mod_amd_ws: a compiled FreeSWITCH module (1.10+) with a one-command install and no mod_audio_fork to build. It adds theamd_ws dialplan application, background detection while a call is bridged or in an IVR, and a built-in mod_amd fallback if the AMDY service is unreachable. See themod_amd_ws guide.

mod_avmd vs mod_audio_fork

FreeSWITCH ships mod_avmd, a built-in voicemail-beep detector. It listens for the tone at the end of a voicemail greeting, which is a narrower signal than full answering-machine detection: it only fires after the beep, so it cannot tell you a machine picked up before the greeting finishes, and it does nothing for machines with no beep (many mobile carrier voicemail systems, some PBX auto-attendants). It is tone detection, not classification of the answering party.

AMDY integrates via mod_audio_fork instead: it forks the live answer-audio stream to AMDY's WebSocket backend over the full duration of the analysis window (typically 1-3 seconds after answer), and returns a human/machine classification based on the acoustic signature of what picked up, not a single tone. Use mod_avmd only if you need pure voicemail-beep detection as a secondary signal; for AMD routing decisions, use mod_audio_fork or AMDY's own native module, mod_amd_ws, which streams the same audio to the same service from inside FreeSWITCH.

1. Dialplan XML wiring

A typical outbound extension in dialplan/default.xml (or your carrier-specific dialplan context) answers the call, starts the fork, and waits for a result variable before bridging:

<extension name="amdy_outbound">
  <condition field="destination_number" expression="^(.+)$">
    <action application="answer"/>
    <!-- set the call identifier AMDY will report back, before the fork starts -->
    <action application="set" data="effective_caller_id_name=${call_uuid}"/>
    <action application="lua" data="amdy_fork.lua"/>
    <!-- amdy_fork.lua blocks until amdy_result is set on the channel -->
    <action application="execute_extension" data="amdy_route XML default"/>
  </condition>
</extension>

<extension name="amdy_route">
  <condition field="${amdy_result}" expression="^HUMAN$">
    <action application="bridge" data="user/${dialed_extension}"/>
  </condition>
  <condition field="${amdy_result}" expression="^MACHINE$">
    <action application="transfer" data="voicemail-drop XML default"/>
  </condition>
  <condition field="${amdy_result}" expression="^(FAS|TIMEOUT|ERROR)$">
    <action application="hangup" data="NORMAL_CLEARING"/>
  </condition>
</extension>

The amdy_fork.lua driver script (installed by the setup script) wraps the raw uuid_audio_fork API call and sets amdy_result on the channel when AMDY returns a classification. If you are wiring this by hand instead of through Lua, the raw API call is:

uuid_audio_fork <uuid> start ws://api.amdy.io:2700 mono 8k {"config":{"sample_rate":8000,"VID":"<call identifier>"}}

2. ESL hooks for external routing

If call routing lives in an external controller rather than the dialplan, connect over the Event Socket and subscribe to the custom event AMDY's fork driver fires when a result lands, instead of polling channel variables:

# inbound ESL connection from your controller
events plain CUSTOM amdy::result

# event payload emitted by amdy_fork.lua on completion
Event-Name: CUSTOM
Event-Subclass: amdy::result
Unique-ID: 3f9a2b6c-...
amdy-status: HUMAN
amdy-cause: HUMAN
amdy-vid: campaign42-lead9981

A minimal ESL client pattern (Python, using the ESL module) for picking up that event and driving routing externally:

import ESL

con = ESL.ESLconnection("127.0.0.1", "8021", "ClueCon")
con.events("plain", "CUSTOM amdy::result")

while True:
    e = con.recvEvent()
    if e:
        status = e.getHeader("amdy-status")
        uuid = e.getHeader("Unique-ID")
        if status == "HUMAN":
            con.api("uuid_transfer", f"{uuid} agent_queue XML default")
        else:
            con.api("uuid_kill", uuid)

This is the same pattern used for any FreeSWITCH ESL-driven dialer: subscribe, filter on the subclass, act on the header. It keeps AMD routing logic in your controller instead of duplicating it in dialplan XML across every context.

3. Result values and routing

amdy_resultAction
HUMANBridge to agent
MACHINETransfer to voicemail-drop extension
FASHangup (CALL_REJECTED)
TIMEOUTHangup (RECOVERY_ON_TIMER_EXPIRE)
ERRORHangup (NETWORK_OUT_OF_ORDER)

Only bridge on HUMAN. Every other value should dispose of the call automatically, same rule as the FreeSWITCH install guide and the ViciDial HUMAN,HUMAN container setting. Full status definitions: API reference.

4. Codec and audio gotchas

  • Fork audio direction. uuid_audio_fork can stream either leg. For AMD you want the read direction (audio coming from the far end / the answering party), not the write direction. Confirm the direction argument in your fork call matches what your build of mod_audio_fork expects; the argument order changed between module versions.
  • Native bridge codec vs 8kHz fork. If the call is bridged natively in a wideband codec (Opus, G.722) to avoid transcoding, the fork still needs an 8kHz mono PCM copy for AMDY. That resample happens inside mod_audio_fork, but it does add a small CPU cost per concurrent fork; budget for it on boxes running near capacity.
  • Early media / progress. If the carrier sends 183 Session Progress with in-band audio before a true 200 OK, the fork can start on ringback or carrier announcements instead of the actual answer. Gate the uuid_audio_fork start call on the CHANNEL_ANSWER event, not on session progress, to avoid streaming pre-answer audio into the detection window.
  • Fork already running on the channel. Only one active fork per channel/direction is supported; a leftover fork from a prior test (call recording, monitoring) on the same leg will block or conflict with the AMD fork. Check uuid_audio_fork <uuid> stop is called for any other fork before starting AMDY's.

Common problems

SymptomLikely cause
amdy_result never set, extension times outamdy::result event not reaching the dialplan; check ESL subscription and that amdy_fork.lua ran
ESL client sees no amdy::result eventsSubscribed to the wrong event class; confirm CUSTOM amdy::result, not a generic CHANNEL_EXECUTE_COMPLETE filter
Detection fires on ringback, not the actual pickupFork started on session progress instead of CHANNEL_ANSWER
Every call logs VID as blank/UnknownCaller ID name or channel var not set before the fork starts
High CPU under load with many concurrent AMD callsWideband-to-8kHz resample cost per fork; check core concurrent session limits and fork count

Support

Need help wiring this into your call flow? Email [email protected] with your FreeSWITCH version (fs_cli -x "version"), the relevant dialplan XML, and the output of fs_cli -x "module_exists mod_audio_fork".

FreeSWITCH AMD Integration Guide — mod_audio_fork, ESL, and Dialplan XML | AMDY.IO