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 inbound mode, your application is the client and FreeSWITCH is the server. Your code opens a TCP connection to FreeSWITCH’s Event Socket port (default 8021), authenticates with the configured password, and then has full access to FreeSWITCH’s API surface — sending commands, subscribing to events for any active call, originating new calls, and manipulating existing sessions. This is the most common ESL usage pattern: a single long-lived connection from a management process that oversees the entire FreeSWITCH instance.
Configuration
ESL is configured in conf/autoload_configs/event_socket.conf.xml. The relevant parameters for inbound mode are:
<configuration name="event_socket.conf" description="Socket Client">
<settings>
<param name="nat-map" value="false"/>
<param name="listen-ip" value="::"/>
<param name="listen-port" value="8021"/>
<param name="password" value="ClueCon"/>
<!--<param name="apply-inbound-acl" value="loopback.auto"/>-->
<!--<param name="stop-on-bind-error" value="true"/>-->
</settings>
</configuration>
| Parameter | Default | Description |
|---|
listen-ip | :: | IP address to bind on. :: listens on all interfaces (IPv4 + IPv6). Use 127.0.0.1 to restrict to loopback only. |
listen-port | 8021 | TCP port ESL listens on |
password | ClueCon | Authentication password. Change this in production. |
apply-inbound-acl | (disabled) | Name of an ACL (from acl.conf.xml) to restrict connecting IPs |
stop-on-bind-error | (disabled) | If true, FreeSWITCH will refuse to start when the ESL port cannot be bound |
After editing the config, reload it without restarting:
fs_cli -x "reloadxml"
fs_cli -x "reload mod_event_socket"
Connecting
Use the ESLconnection constructor with (host, port, password). The constructor blocks until authentication completes. Check connected() before sending any commands.
import ESL
con = ESL.ESLconnection('127.0.0.1', '8021', 'ClueCon')
if con.connected():
print("Connected to FreeSWITCH")
else:
print("Connection failed")
You can also test connectivity with a raw telnet session to verify the server is reachable and the password is correct before writing code:
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Content-Type: auth/request
auth ClueCon
Content-Type: command/reply
Reply-Text: +OK accepted
api status
Content-Type: api/response
Content-Length: 182
UP 0 years, 0 days, 1 hours, 23 minutes, 45 seconds, 123 milliseconds, 456 microseconds
FreeSWITCH (Version 1.10.x) is ready
...
Sending API Commands
ESL exposes two API execution methods depending on whether you need the result immediately.
Synchronous: api()
api() sends the command and blocks until FreeSWITCH returns the response. Use it for quick administrative queries where you need the answer before proceeding.
import ESL
con = ESL.ESLconnection('127.0.0.1', '8021', 'ClueCon')
if con.connected():
# Synchronous: blocks until FreeSWITCH replies
e = con.api('status')
print(e.getBody())
# With arguments
e = con.api('sofia', 'status')
print(e.getBody())
Background: bgapi()
bgapi() returns immediately and executes the command in a FreeSWITCH background thread. The result arrives later as a BACKGROUND_JOB event containing a Job-UUID that matches what bgapi() returned.
import ESL
con = ESL.ESLconnection('127.0.0.1', '8021', 'ClueCon')
if con.connected():
con.events('plain', 'BACKGROUND_JOB')
# Fire and forget — result comes back as an event
e = con.bgapi('originate', 'sofia/internal/1001@pbx.example.com &echo')
job_uuid = e.getHeader('Job-UUID')
print(f"Job dispatched: {job_uuid}")
# Collect the result event
result = con.recvEvent()
if result and result.getHeader('Job-UUID') == job_uuid:
print(result.getBody())
Use bgapi() for any command that takes more than a few milliseconds — especially originate, bridge, and calls to external services. Blocking the ESL connection with a long-running api() call prevents your process from receiving other events while it waits.
Subscribing to Events
After connecting, subscribe to event streams using events(). FreeSWITCH will push matching events to your connection as they occur.
Subscribe to all events
con.events('plain', 'all')
Subscribe to specific events only
# Only receive CHANNEL_ANSWER and DTMF events
con.events('plain', 'CHANNEL_ANSWER DTMF')
filter() narrows delivery further — only events where the named header matches the given value will be sent:
# Only events for this specific call UUID
con.filter('Unique-ID', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890')
Event loop example
import ESL
con = ESL.ESLconnection('127.0.0.1', '8021', 'ClueCon')
if con.connected():
con.events('plain', 'all')
while con.connected():
# Block until an event arrives
e = con.recvEvent()
if e:
name = e.getHeader('Event-Name')
uuid = e.getHeader('Unique-ID')
print(f"Event: {name} UUID: {uuid}")
if name == 'CHANNEL_HANGUP':
cause = e.getHeader('Hangup-Cause')
print(f" → Call {uuid} hung up: {cause}")
Using recvEventTimed() for a non-blocking loop
import ESL
import time
con = ESL.ESLconnection('127.0.0.1', '8021', 'ClueCon')
if con.connected():
con.events('plain', 'CHANNEL_ANSWER CHANNEL_HANGUP DTMF')
while con.connected():
# Wait up to 500 ms for an event, then do other work
e = con.recvEventTimed(500)
if e:
print(f"Event: {e.getHeader('Event-Name')}")
else:
# No event within the timeout — do housekeeping here
pass
Originating Calls
Use api('originate', ...) or bgapi('originate', ...) to place outbound calls.
The originate command syntax is:
originate <call-url> <exten>|&<app>(args) [<dialplan>] [<context>] [<cid_name>] [<cid_num>] [<timeout>]
Examples
import ESL
con = ESL.ESLconnection('127.0.0.1', '8021', 'ClueCon')
if con.connected():
# Call a SIP extension and send it to dialplan extension 1000 in default context
e = con.bgapi('originate',
'sofia/internal/1001@pbx.example.com 1000 XML default '
'"Test Call" 5551234567 30'
)
print(f"Originate job: {e.getHeader('Job-UUID')}")
# Call and immediately connect to an application instead of dialplan
e = con.bgapi('originate',
'sofia/internal/1001@pbx.example.com &playback(/usr/local/freeswitch/sounds/en/us/callie/ivr/ivr-welcome.wav)'
)
print(f"Playback job: {e.getHeader('Job-UUID')}")
Controlling Active Calls
Once a call is active you can manipulate it using ESL API commands that target its UUID. Retrieve the UUID from incoming events (the Unique-ID header) or from the originate BACKGROUND_JOB response body.
import ESL
con = ESL.ESLconnection('127.0.0.1', '8021', 'ClueCon')
if con.connected():
uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
# Hang up a call
e = con.api('uuid_kill', uuid)
print(e.getBody())
# Bridge two calls together
other_uuid = 'b2c3d4e5-f6a7-8901-bcde-f01234567891'
e = con.api('uuid_bridge', f'{uuid} {other_uuid}')
print(e.getBody())
# Transfer a call to a new dialplan destination
e = con.api('uuid_transfer', f'{uuid} 2000 XML default')
print(e.getBody())
# Answer a ringing call leg
e = con.api('uuid_answer', uuid)
print(e.getBody())
# Play a file to one leg of a call
e = con.api('uuid_broadcast',
f'{uuid} /usr/local/freeswitch/sounds/en/us/callie/ivr/ivr-hold_connect_call.wav aleg'
)
print(e.getBody())
Common UUID commands
| Command | Syntax | Description |
|---|
uuid_kill | uuid_kill <uuid> [cause] | Hang up the call with an optional hangup cause |
uuid_bridge | uuid_bridge <uuid1> <uuid2> | Bridge two existing call legs together |
uuid_transfer | uuid_transfer <uuid> <dest> [<dialplan>] [<context>] | Transfer the call to a new dialplan destination |
uuid_answer | uuid_answer <uuid> | Answer a ringing inbound call |
uuid_broadcast | uuid_broadcast <uuid> <path> [aleg|bleg|both] | Play audio to a call leg |
uuid_hold | uuid_hold <uuid> | Place a call on hold |
uuid_park | uuid_park <uuid> | Park a call |
uuid_getvar | uuid_getvar <uuid> <var> | Read a channel variable |
uuid_setvar | uuid_setvar <uuid> <var> <value> | Set a channel variable |