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 FreeSWITCH, every individual call leg is represented by two inseparable objects: a session (switch_core_session_t) that owns the media resources and the execution thread, and a channel (switch_channel_t) that holds the signaling state, variables, and caller information for that leg. Understanding the distinction between these two objects — and how they interact — is fundamental to working with FreeSWITCH at any level.
A session is the media container: it owns the codec handles, the message queue, the memory pool, and the thread that runs the call. A channel is the signaling and state container: it holds the call state machine, channel flags, channel variables, and the caller profile. The channel always belongs to exactly one session, and a session always has exactly one channel.

Sessions

A switch_core_session_t (defined in src/include/switch_types.h as an opaque struct switch_core_session) is the top-level object for a call leg. It contains:
  • A pointer to its associated switch_channel_t.
  • Codec handles for the read (inbound) and write (outbound) audio streams.
  • A message queue for inter-session communication (e.g., bridge signals, DTMF forwarding).
  • A memory pool whose lifetime matches the call — all per-call allocations use this pool.
  • The codec implementation tables for the current negotiated codec.
  • An application log (switch_app_log) recording every application executed on the session.
Sessions are created by endpoint modules (e.g., mod_sofia) when a call arrives or is originated. The core registers the session in a global hash table, making it findable by UUID from anywhere in the system.
/* Retrieve a session by UUID (from switch_core.h) */
#define switch_core_session_locate(uuid_str) \
    switch_core_session_perform_locate(uuid_str, __FILE__, __SWITCH_FUNC__, __LINE__)

/* Get the UUID string for a session */
SWITCH_DECLARE(char *) switch_core_session_get_uuid(switch_core_session_t *session);

/* Get the channel from a session */
SWITCH_DECLARE(switch_channel_t *) switch_core_session_get_channel(
    switch_core_session_t *session
);
Always call switch_core_session_rwunlock() after switch_core_session_locate(). The locate call acquires a read lock on the session to prevent it from being destroyed while you hold a pointer to it. Failing to unlock will cause the session to hang at hangup time.

Channels

The switch_channel_t (defined in src/include/switch_channel.h) owns the call’s state machine. Every channel progresses through a defined sequence of states; the state at any moment determines what operations are legal and what event will fire next.

Channel State Machine

The switch_channel_state_t enum (from src/include/switch_types.h) defines all valid states:
/*
 * Channel States
 * CS_SOFT_EXECUTE, CS_EXCHANGE_MEDIA, and CS_CONSUME_MEDIA
 * are often overridden by specific applications.
 */
typedef enum {
    CS_NEW,            /* Channel is newly created */
    CS_INIT,           /* Channel has been initialized */
    CS_ROUTING,        /* Channel is looking for an extension to execute */
    CS_SOFT_EXECUTE,   /* Channel is ready to execute from 3rd party control */
    CS_EXECUTE,        /* Channel is executing its dialplan */
    CS_EXCHANGE_MEDIA, /* Channel is exchanging media with another channel */
    CS_PARK,           /* Channel is accepting media awaiting commands */
    CS_CONSUME_MEDIA,  /* Channel is consuming all media and dropping it */
    CS_HIBERNATE,      /* Channel is in a sleep state */
    CS_RESET,          /* Channel is in a reset state */
    CS_HANGUP,         /* Channel is flagged for hangup and ready to end */
    CS_REPORTING,      /* Channel is ready to collect call detail */
    CS_DESTROY,        /* Channel is ready to be destroyed and out of the state machine */
    CS_NONE
} switch_channel_state_t;
The typical lifecycle for an inbound call follows this path:
CS_NEW → CS_INIT → CS_ROUTING → CS_EXECUTE → CS_EXCHANGE_MEDIA → CS_HANGUP → CS_REPORTING → CS_DESTROY
Channel state is read with switch_channel_get_state() and tested with convenience macros:
/* From switch_channel.h */
SWITCH_DECLARE(switch_channel_state_t) switch_channel_get_state(switch_channel_t *channel);

/* Convenience macros */
#define switch_channel_ready(_channel) \
    switch_channel_test_ready(_channel, SWITCH_TRUE, SWITCH_FALSE)

#define switch_channel_media_ready(_channel) \
    switch_channel_test_ready(_channel, SWITCH_TRUE, SWITCH_TRUE)

#define switch_channel_media_up(_channel) \
    (switch_channel_test_flag(_channel, CF_ANSWERED) || \
     switch_channel_test_flag(_channel, CF_EARLY_MEDIA))

Channel Timetable

The channel records timestamps for key lifecycle events in its switch_channel_timetable_t:
/* From switch_channel.h */
struct switch_channel_timetable {
    switch_time_t profile_created;
    switch_time_t created;
    switch_time_t answered;
    switch_time_t progress;
    switch_time_t progress_media;
    switch_time_t hungup;
    switch_time_t transferred;
    switch_time_t resurrected;
    switch_time_t bridged;
    switch_time_t last_hold;
    switch_time_t hold_accum;
    struct switch_channel_timetable *next;
};
These timestamps are used by CDR modules to compute call duration, ring time, and hold time.

Channel Variables

Channel variables are a key-value store on the channel — essentially a hash map of char * strings. They are the primary mechanism for communicating state between dialplan steps, between modules, and across call legs. Variables are set and read through the channel API:
/* From switch_channel.h */
#define switch_channel_set_variable(_channel, _var, _val) \
    switch_channel_set_variable_var_check(_channel, _var, _val, SWITCH_TRUE)

#define switch_channel_get_variable(_channel, _varname) \
    switch_channel_get_variable_dup(_channel, _varname, SWITCH_TRUE, -1)
In the XML dialplan, the set application writes a variable and ${variable_name} reads it:
<!-- Set a variable on the A-leg channel -->
<action application="set" data="my_greeting=hello"/>

<!-- Reference it later in the same extension -->
<action application="log" data="INFO My greeting is: ${my_greeting}"/>
Well-known channel variables include:
VariableSet byMeaning
uuidCoreThe session UUID
caller_id_nameEndpointCaller’s display name
caller_id_numberEndpointCaller’s number
destination_numberEndpointDialed number
sip_to_usermod_sofiaSIP To: user portion
hangup_causeCoreReason for call termination
bridge_uuidCoreUUID of the bridged B-leg
recording_follow_transferApplicationWhether recording follows a transfer

Bridging

Bridging connects two sessions so that audio flows between them. The core function is switch_ivr_uuid_bridge(), which takes the UUIDs of two existing sessions and connects their media paths.
/* From switch_ivr.h */
SWITCH_DECLARE(switch_status_t) switch_ivr_uuid_bridge(
    const char *originator_uuid,
    const char *originatee_uuid
);
When the bridge dialplan application is used, it:
  1. Originates a new B-leg session (using the endpoint specified in the bridge string).
  2. Places both the A-leg and B-leg into CS_EXCHANGE_MEDIA.
  3. Connects the RTP engines of the two legs so audio flows between them.
If the two parties use different codecs, FreeSWITCH transcodes in the core RTP engine. If they share a codec, FreeSWITCH can optionally bypass the media (pass audio directly without decoding) using the bypass_media channel variable.
<!-- Bridge to a SIP gateway -->
<action application="bridge" data="sofia/gateway/mytrunk/${destination_number}"/>

<!-- Bridge with bypass media (no transcoding — direct RTP) -->
<action application="set" data="bypass_media=true"/>
<action application="bridge" data="sofia/gateway/mytrunk/${destination_number}"/>

UUID

Every session is assigned a UUID (Universally Unique Identifier) at the moment it is created. The UUID is the primary key used to reference a specific call from any part of the system:
  • ESL / API commands: uuid_kill <uuid>, uuid_transfer <uuid>, uuid_record <uuid>.
  • Channel variables: ${uuid} and ${bridge_uuid} are set automatically.
  • CDR records: the UUID links A-leg and B-leg CDRs for a complete call record.
  • Event headers: every channel event includes a Unique-ID header with the session UUID.
# Hang up a specific call by UUID via the console API
uuid_kill 3a8f1d2e-0c4b-4e7f-a19c-2b8d3e5f6a1c

# Transfer a call to extension 1000 in the default context
uuid_transfer 3a8f1d2e-0c4b-4e7f-a19c-2b8d3e5f6a1c 1000 XML default

Call Parks and Transfers

FreeSWITCH provides several mechanisms for moving calls between agents or placing them in a waiting state.
A blind transfer immediately redirects the A-leg to a new destination without waiting to confirm the target is available. The transfer dialplan application performs a blind transfer within FreeSWITCH:
<action application="transfer" data="1001 XML default"/>
The core function switch_ivr_session_transfer() handles moving the session to a new context/extension:
SWITCH_DECLARE(switch_status_t) switch_ivr_session_transfer(
    switch_core_session_t *session,
    const char *extension,
    const char *dialplan,
    const char *context
);
An attended transfer places the original caller on hold, dials the transfer target, waits for the agent to speak with the target, and then completes the transfer. This is typically handled by the SIP endpoint via SIP REFER with the Replaces header, but can also be orchestrated by mod_dptools.
Parking places a call in a holding state where it waits to be retrieved. FreeSWITCH provides two park mechanisms:
  • switch_ivr_park() — generic park that holds the session in a loop.
  • mod_fifo — a queue-based parking system where callers wait in named queues and agents log in to retrieve them.
/* Park a session (from switch_ivr.h) */
SWITCH_DECLARE(switch_status_t) switch_ivr_park(
    switch_core_session_t *session,
    switch_input_args_t *args
);

Build docs developers (and LLMs) love