Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/signalwire/freeswitch/llms.txt

Use this file to discover all available pages before exploring further.

In outbound mode, the roles are reversed: FreeSWITCH is the client and your application is the server. When an incoming (or originated) call matches a dialplan extension that uses the socket application, FreeSWITCH opens a TCP connection to your server and hands over control of that call leg. Your server then drives the call — answering, playing audio, collecting DTMF, recording, transferring, or hanging up — using the same ESLconnection API used in inbound mode, but scoped entirely to the single call that was connected. Outbound mode is ideal for per-call IVR logic, voicemail, call recording, and any scenario where each call needs independent, sequential application control without a global event subscription.

How It Works

1

Call arrives in the dialplan

FreeSWITCH processes the call and reaches a dialplan extension containing the socket application.
2

FreeSWITCH connects to your server

FreeSWITCH opens a TCP connection to the host and port specified in the socket application’s data attribute.
3

FreeSWITCH sends a connect event

FreeSWITCH sends a Content-Type: command/reply with a full CHANNEL_DATA event body describing the call. Your server reads this via getInfo().
4

Your server controls the call

Your server uses ESLconnection(socket_fd) to wrap the accepted socket, then calls execute() to run dialplan applications — answer, playback, play_and_get_digits, hangup, etc.
5

Call ends or is transferred

When your server is done it can hang up the call, transfer it back to the dialplan, or simply close the socket to return control to FreeSWITCH.

Dialplan Configuration

Add a socket action to any dialplan extension. The data attribute specifies your server’s address, port, and optional flags.
<extension name="outbound-esl-demo">
  <condition field="destination_number" expression="^(1234)$">
    <action application="socket" data="127.0.0.1:8084 async full"/>
  </condition>
</extension>
The data value format is: <host>:<port> [async] [full]
FlagDescription
asyncExecute socket commands asynchronously. The call continues processing while your server handles events. Without async, each execute() call blocks until the application completes before the next command is sent.
fullSend full event headers to your server (equivalent to myevents on an inbound connection). Without full, only minimal event data is delivered.
Both flags can be combined: data="127.0.0.1:8084 async full" is the most common form.
Without the async flag, execute() calls are serialised: FreeSWITCH will not process the next command until the current application finishes. This makes synchronous mode simpler to reason about for linear IVR flows but means your server blocks between steps. With async, you receive events for each application completion and must track state yourself.

Writing an Outbound Server

Your server is a standard TCP server. For each accepted connection, create an ESLconnection from the raw socket file descriptor, call getInfo() to retrieve the initial session data, then issue execute() calls to control the call.

Python outbound server

The following example is adapted from libs/esl/python3/server.py:
#!/usr/bin/env python3

import socketserver
import ESL

class ESLRequestHandler(socketserver.BaseRequestHandler):
    def handle(self):
        print(f"New connection from {self.client_address}")

        # Wrap the accepted socket as an ESLconnection
        fd = self.request.fileno()
        con = ESL.ESLconnection(fd)

        if not con.connected():
            print("ESL connection failed")
            return

        # Retrieve the initial session info (CHANNEL_DATA event)
        info = con.getInfo()
        uuid = info.getHeader("unique-id")
        dest = info.getHeader("Caller-Destination-Number")
        print(f"Handling call {uuid}{dest}")

        # Subscribe to events for this call only
        con.sendRecv("myevents")

        # Answer the call
        con.execute("answer", "", uuid)

        # Play a greeting
        con.execute(
            "playback",
            "/usr/local/freeswitch/sounds/en/us/callie/ivr/ivr-welcome.wav",
            uuid
        )

        # Collect up to 1 digit, 5-second timeout, 3 tries
        con.execute(
            "play_and_get_digits",
            "1 1 3 5000 # "
            "/usr/local/freeswitch/sounds/en/us/callie/ivr/ivr-enter_ext_pound.wav "
            "/usr/local/freeswitch/sounds/en/us/callie/ivr/ivr-that_was_an_invalid_entry.wav "
            "dtmf_digit \\d",
            uuid
        )

        # Read the digit from the channel variable
        digit_event = con.sendRecv(f"getvar dtmf_digit")
        digit = digit_event.getBody().strip() if digit_event else ""
        print(f"Caller pressed: {digit!r}")

        # Route based on input
        if digit == "1":
            con.execute("transfer", "1001 XML default", uuid)
        elif digit == "2":
            con.execute("transfer", "1002 XML default", uuid)
        else:
            con.execute(
                "playback",
                "/usr/local/freeswitch/sounds/en/us/callie/ivr/ivr-invalid_sound.wav",
                uuid
            )
            con.execute("hangup", "", uuid)


if __name__ == "__main__":
    HOST, PORT = "0.0.0.0", 8084
    server = socketserver.ThreadingTCPServer((HOST, PORT), ESLRequestHandler)
    server.allow_reuse_address = True
    print(f"Outbound ESL server listening on {HOST}:{PORT}")
    server.serve_forever()

Perl outbound server

Adapted from libs/esl/perl/server.pl:
#!/usr/bin/perl
require ESL;
use IO::Socket::INET;

my $sock = new IO::Socket::INET(
    LocalHost => '127.0.0.1',
    LocalPort => '8040',
    Proto     => 'tcp',
    Listen    => 1,
    Reuse     => 1,
) or die "Could not create socket: $!\n";

for(;;) {
    my $new_sock = $sock->accept();
    my $pid = fork();
    if ($pid) {
        close($new_sock);
        next;
    }

    my $fd  = fileno($new_sock);
    my $con = new ESL::ESLconnection($fd);
    my $info = $con->getInfo();

    my $uuid = $info->getHeader("unique-id");
    printf "Connected call %s from %s\n",
        $uuid, $info->getHeader("caller-caller-id-number");

    $con->execute("answer",   "", $uuid);
    $con->execute("playback", "/ram/swimp.raw", $uuid);

    while ($con->connected()) {
        my $e = $con->recvEvent();
        if ($e) {
            my $name = $e->getHeader("event-name");
            print "EVENT [$name]\n";
            if ($name eq "DTMF") {
                my $digit    = $e->getHeader("dtmf-digit");
                my $duration = $e->getHeader("dtmf-duration");
                print "DTMF digit $digit ($duration)\n";
            }
        }
    }

    print "Call ended\n";
    close($new_sock);
}

Session Commands in Outbound Mode

Use con.execute(app, args, uuid) to run any FreeSWITCH dialplan application on the connected call. The uuid can be retrieved from getInfo().getHeader("unique-id").
# Answer the call
con.execute("answer", "", uuid)

# Play an audio file
con.execute("playback", "/path/to/audio.wav", uuid)

# Collect DTMF digits
# Format: min_digits max_digits max_tries timeout terminator
#         prompt_file invalid_file variable_name regexp
con.execute(
    "play_and_get_digits",
    "1 4 3 10000 # prompt.wav invalid.wav menu_choice \\d+",
    uuid
)

# Record the caller
con.execute("record", "/tmp/recording.wav 60 200 3", uuid)

# Transfer to another extension
con.execute("transfer", "2000 XML default", uuid)

# Hang up
con.execute("hangup", "", uuid)

# Play text-to-speech (requires a TTS engine module)
con.execute("speak", "flite|kal|Welcome to FreeSWITCH", uuid)

# Sleep (pause execution for ms milliseconds)
con.execute("sleep", "2000", uuid)

Common dialplan applications for outbound mode

ApplicationKey argumentsDescription
answerAnswer the call
playback<path>Play an audio file
play_and_get_digits<min> <max> <tries> <timeout> <term> <prompt> <invalid> <var> <regexp>Collect DTMF with prompts
record<path> [max_sec] [silence_threshold] [silence_seconds]Record the call audio
bridge<dial-string>Bridge this leg to another destination
transfer<dest> [<dialplan>] [<context>]Transfer the call to the dialplan
sleep<ms>Pause execution for the given number of milliseconds
hangup[<cause>]Hang up the call
speak<engine>|<voice>|<text>Text-to-speech playback
set<variable>=<value>Set a channel variable

Event Handling in Outbound Mode

By default the outbound socket delivers only basic call control events. Use myevents to receive all events for this call, and linger to keep the connection open after the call hangs up (useful for post-call processing).
# Subscribe to all events for this specific call
con.sendRecv("myevents")

# Keep the socket open after the call hangs up
con.sendRecv("linger")

# Now run your application logic and listen for events
while con.connected():
    e = con.recvEventTimed(1000)
    if not e:
        continue

    name = e.getHeader("Event-Name")
    print(f"Event: {name}")

    if name == "DTMF":
        digit = e.getHeader("DTMF-Digit")
        print(f"DTMF digit: {digit}")

    elif name == "CHANNEL_HANGUP":
        cause = e.getHeader("Hangup-Cause")
        print(f"Call hung up: {cause}")
        break

    elif name == "SERVER_DISCONNECTED":
        break
The Ruby example in libs/esl/ruby/server3.rb demonstrates this pattern with divert_events on to capture DTMF events during playback:
@con.sendRecv("myevents")
@con.sendRecv("divert_events on")

@con.execute("answer", "")
@con.execute("playback", "/usr/local/freeswitch/sounds/music/8000/suite-espanola-op-47-leyenda.wav")

while @con.connected
  e = @con.recvEvent
  next unless e

  name = e.getHeader("Event-Name")
  break if name == "SERVER_DISCONNECTED"

  if name == "DTMF"
    digit = e.getHeader("DTMF-Digit")
    duration = e.getHeader("DTMF-Duration")
    puts "DTMF: #{digit} (#{duration})"
    @con.execute("transfer", "99355151") if digit == "9"
  end
end

Build docs developers (and LLMs) love