Voicemail & Missed Call Service
π Back to Main Documentation
OmniTAS provides its own voicemail service for deposit (leaving a message) and retrieval
(listening to messages). The interactive flow is driven entirely by the TAS over the FreeSWITCH
Event Socket Library (ESL), rather than by FreeSWITCH's built-in mod_voicemail IVR. Storage stays
compatible with mod_voicemail, so the voicemail web UI, REST API, and message-waiting (MWI) SMS
notifications continue to work unchanged.
Related Documentationβ
Core Documentationβ
- π Main README - Overview and quick start
- π§ Configuration Guide - Voicemail configuration (timezone, SMSc, notification templates)
- π§ Operations Guide - Voicemail management in Control Panel
Call Processing Integrationβ
- π Dialplan Configuration - Voicemail deposit/retrieval in dialplan
- βοΈ Supplementary Services - Call forward on busy/no-answer to voicemail
- π TTS Prompts - Voicemail greeting prompts
Related Servicesβ
- π’ Number Translation - Voicemail access number translation
Monitoringβ
- π Metrics Reference - Voicemail usage metrics
Architectureβ
The TAS already keeps inbound ESL connections to FreeSWITCH for events, commands, and monitoring. Voicemail adds a second, independent path: an outbound socket listener that FreeSWITCH connects into, per call, when the dialplan hands a call to voicemail. Over that per-call socket the TAS plays prompts, records audio, and collects DTMF directly.
Key points:
- The inbound ESL path (call events, online charging, monitoring) is unchanged.
- The outbound socket is used only for the interactive voicemail IVR.
- The TAS and FreeSWITCH are co-located, so the outbound socket binds to loopback.
- Storage uses the
mod_voicemaildatabase schema, sovm_boxcount, the web UI, and the REST API keep working without modification.mod_voicemailmust remain loaded in FreeSWITCH so the database and thevm_boxcountcommand stay available.
Routing a Call to Voicemailβ
Voicemail is added in the XML dialplan as needed; it is not active unless your dialplan routes a call to it. The dialplan sets three channel variables and then hands the call to the outbound socket, which the TAS reads to decide what to do.
| Variable | Set when | Description |
|---|---|---|
tas_vm_mode | Always | deposit to leave a message, retrieve to listen to messages, or greeting to record a personal greeting (see Personal Greetings). Selects the flow. |
tas_vm_mailbox | Always | Mailbox owner's MSISDN. For deposit, the called subscriber; for retrieval, the calling subscriber (the box owner). |
tas_vm_caller | Deposit | Calling party's number, stored with the message and used in the notification text. |
default_language | Optional | Language used for the spoken "received at" date/time during retrieval. Set per subscriber in the dialplan; falls back to the say_language config value when unset. |
The socket application makes FreeSWITCH connect to the TAS voicemail listener. The IP and port
must match the outbound_socket configuration. The arguments are FreeSWITCH-standard: async
lets the TAS receive events while applications run, and full grants full event access.
Deposit exampleβ
Put this after a bridge command so it runs when the bridge fails (no answer / unreachable):
<action application="log"
data="INFO Failed to bridge Call - Routing to Call Forward No-Answer Destination" />
<action application="set"
data="sip_h_History-Info=<sip:${destination_number}@${ims_domain}>;index=1.1" />
<action application="set" data="sip_call_id=${sip_call_id};CALL_FORWARD_NO_ANSWER" />
<action application="log" data="DEBUG Called Voicemail Deposit Number for ${msisdn}" />
<action application="set" data="default_language=fr"/>
<action application="answer" />
<action application="sleep" data="500"/>
<!-- Hand the call to the TAS voicemail IVR (deposit). The TAS records the message,
writes the mod_voicemail row, and fires the MWI itself.
ip:port MUST match :tas voicemail.outbound_socket in runtime.exs. -->
<action application="set" data="tas_vm_mode=deposit"/>
<action application="set" data="tas_vm_mailbox=${msisdn}"/>
<action application="set" data="tas_vm_caller=${effective_caller_id_number}"/>
<action application="socket" data="127.0.0.1:8084 async full"/>
When a call is forwarded to voicemail, deposit to the original called party using the History-Info value (e.g.
tas_vm_mailbox=${history_info_value}), not the voicemail service number. See Dialplan Configuration for History-Info handling.
Retrieval exampleβ
<extension name="Static-Route-Voicemail-Check">
<condition field="${tas_destination_number}" expression="^(2222|55512411520)$">
<action application="log" data="DEBUG Called Voicemail Check Number" />
<action application="set" data="default_language=fr"/>
<action application="answer" />
<!-- Hand the call to the TAS voicemail IVR (retrieval). The TAS plays messages,
handles the hear/delete/save menu, and clears the MWI.
ip:port MUST match :tas voicemail.outbound_socket in runtime.exs. -->
<action application="set" data="tas_vm_mode=retrieve"/>
<action application="set" data="tas_vm_mailbox=${msisdn}"/>
<action application="socket" data="127.0.0.1:8084 async full"/>
</condition>
</extension>
Greeting-recording exampleβ
Route a dedicated access number to the greeting flow (only needed when greetings.enabled). The
mailbox is the calling subscriber, who is recording their own greeting:
<extension name="Static-Route-Voicemail-Greeting">
<condition field="${tas_destination_number}" expression="^(2223)$">
<action application="log" data="DEBUG Called Voicemail Greeting-Record Number" />
<action application="set" data="default_language=fr"/>
<action application="answer" />
<!-- Hand the call to the TAS voicemail IVR (record personal greeting). The TAS plays the
record prompt, records, and stores the greeting for the mailbox.
ip:port MUST match :tas voicemail.outbound_socket in runtime.exs. -->
<action application="set" data="tas_vm_mode=greeting"/>
<action application="set" data="tas_vm_mailbox=${msisdn}"/>
<action application="socket" data="127.0.0.1:8084 async full"/>
</condition>
</extension>
Greeting-Recording Flowβ
- Answer the call.
- Play the record-greeting prompt (
prompts.greeting_record). - Play the beep (
prompts.beep) and record, ending on the terminator key, silence, or the maximum length (the samerecordlimits as a deposit). - If the recording meets
record.min_seconds, any existing greeting is cleared cluster-wide first (so no stale copy lingers on another TAS β see the multi-TAS note below), the new one is stored asgreeting.wavunder the mailbox's directory instorage_dir, andprompts.greeting_savedis played; otherwise the existing greeting is kept unchanged.
The deposit flow uses this file automatically: when a greeting exists for the called mailbox it is
played instead of prompts.deposit_greeting. To remove a recorded greeting (revert to the default),
see Clearing a greeting.
Deposit Flowβ
- Answer the call.
- Play the greeting. If the subscriber has recorded a personal greeting
it is played; otherwise the default
prompts.deposit_greetingis used. - Play the beep (
prompts.beep). - Record to
storage_dir, ending on caller hangup, the terminator key, silence, or the maximum length. - If the recording is at least
record.min_seconds, store it as a new (unread) message; otherwise it is discarded and treated as a missed call. - Send the MWI notification.
The notification is sent immediately after the message is stored. This ordering is deliberate: the caller's own hangup ends the recording, so the message must be written before the notification is generated, so the waiting-message count is accurate.
Retrieval Flowβ
Per-message menu:
| Key | Action | Effect |
|---|---|---|
1 | Hear again | Replays the current message from the top. |
2 | Delete | Removes the message and its recording, plays the "deleted" prompt, advances. |
3 | Save | Keeps the message (already marked read), plays the "saved" prompt, advances. |
4 | Forward | Forwards the recording to another mailbox (see Forwarding a Message). Available when forwarding is enabled. |
| (none / timeout) | β | Keeps the message and advances. |
Playing a message marks it read, so it no longer counts as new. Once the subscriber has been through their messages the mailbox has no unread messages and the MWI is cleared.
The "received at" announcement combines a fixed prompt (prompts.retrieve_received_at, e.g. an
audio file saying "Voicemail received at") with a date/time spoken on the fly by FreeSWITCH's
say module, using the message's stored timestamp. The spoken language is the channel's
default_language (set per subscriber in the dialplan), falling back to the say_language config
value when the channel does not set one. This avoids
needing a pre-recorded file for every possible date/time.
Message Waiting Indication (MWI)β
Notifications are sent through the MWI notifier, which posts to the SMSc's MWI API. It chooses
between a "missed call" and a "voicemail waiting" message based on the number of unread messages in
the mailbox (queried with FreeSWITCH's vm_boxcount).
| Event | Indication | Notification text used |
|---|---|---|
| Deposit, no message left | Inactive (missed call) | voicemail_notification_text.not_left |
| Deposit, one message waiting | Active | voicemail_notification_text.single_voicemail |
| Deposit, multiple messages waiting | Active | voicemail_notification_text.multiple_voicemails |
| Retrieval finished | Cleared | voicemail_notification_text.cleared (or a default) |
Variables available in the notification templates:
| Variable | Available in | Description |
|---|---|---|
caller | All | Calling party number that left the message / missed call. |
day | All | Day of month, in the configured timezone. |
month | All | Month number, in the configured timezone. |
hour | All | Hour (24h), in the configured timezone. |
minute | All | Minute, zero-padded, in the configured timezone. |
message_count | multiple_voicemails | Number of unread messages. Only set when the count is greater than 1. |
Message Storageβ
Deposited messages are stored two ways, mirroring mod_voicemail:
- Audio files are written under
storage_dir, organised per mailbox. - Metadata (owner, caller, timestamp, file path, length, read/unread state) is written as a row
in the FreeSWITCH
voicemail_default.dbSQLite database, in the standardvoicemail_msgstable.
Because the schema matches mod_voicemail, vm_boxcount, the voicemail web UI, and the voicemail
REST API all read these messages without changes. You can view voicemail box usage and message
status from the Control Panel's voicemail tab.
Personal Greetingsβ
By default, callers reaching voicemail hear the system greeting (prompts.deposit_greeting).
Optionally, subscribers can record their own greeting, which is then played instead of the
default for calls to their mailbox.
Personal greetings are opt-in (greetings.enabled). When disabled, or when a subscriber has not
recorded one, the deposit flow falls back to the configured default greeting, so behaviour is
unchanged.
Recording a greetingβ
A subscriber records or replaces their greeting through a dedicated IVR flow selected with
tas_vm_mode=greeting, routed from its own access number in the dialplan the same way deposit and
retrieval are. See the Greeting-recording example dialplan and the
Greeting-Recording Flow for the step-by-step behaviour.
The greeting is stored per mailbox as greeting.wav under that mailbox's directory in storage_dir
(alongside its message recordings). Re-recording overwrites the previous greeting; to remove it and
revert to the default, see Clearing a greeting.
Multi-TAS: a greeting may be recorded on one TAS but a deposit may land on another (see Multi-TAS Voicemail), so the deposit flow locates the greeting cluster-wide β it checks locally first, then fans out to peers β before falling back to the default. To keep this unambiguous, recording a greeting first clears any existing greeting on every TAS, then writes the new one, so exactly one copy exists cluster-wide (a subscriber re-recording on a different node never leaves a stale greeting behind). The greeting is fetched and played the same way a remote message recording is.
Forwarding a Message to Another Mailboxβ
During retrieval, a subscriber can forward the message they are listening to into another
subscriber's mailbox. Forwarding is opt-in (forwarding.enabled); when disabled, key 4 is
ignored.
- The subscriber presses
4and is prompted (prompts.forward_enter_mailbox) to key in the target mailbox MSISDN, terminated with#. - The current recording is copied into the target mailbox as a new, unread message. The
original caller details are preserved and the
forwarded_bycolumn records the forwarding subscriber, so the target sees who forwarded it. - An MWI notification fires to the target mailbox exactly as a normal deposit would.
- The subscriber hears a confirmation (
prompts.forward_done); an unknown/invalid target playsprompts.forward_invalidand returns to the message menu.
The original message is unaffected β forwarding leaves it in the subscriber's own mailbox.
Multi-TAS: the forwarded copy becomes a normal message owned by the node that performed the forward (it copies the WAV locally and inserts the row), and the target's waiting count is computed cluster-wide, so the target sees the forwarded message from whichever TAS they retrieve on.
Message Retention and Expiryβ
Voicemails do not accumulate forever. A periodic sweeper deletes messages (row and recording)
once they exceed a configured age, keeping mailboxes and disk usage bounded. Expiry is opt-in
(expiry.enabled); when disabled, messages are kept until manually deleted.
- The sweeper runs every
expiry.sweep_interval_minutes. - A message is expired when its age (from
created_epoch) exceedsexpiry.max_age_days. - Optionally, already-read messages can expire sooner via
expiry.read_max_age_days(e.g. keep unread messages 30 days but purge listened-to messages after 7). - Expiring a message removes both its
voicemail_msgsrow and its recording file, so the web UI, REST API, and waiting count all reflect the removal.
If expiring messages clears a mailbox's last unread message, the MWI is cleared for that mailbox so the handset badge stays accurate.
Multi-TAS: each node sweeps its own store on its own schedule. Because every message has a single owning node (see below), no cross-node coordination is needed β each owner expires the messages it holds.
Multi-TAS Voicemail (Clustered Mailboxes)β
When more than one OmniTAS serves the same subscribers, a subscriber's calls can land on any TAS at any time β there is no stickiness. Since each TAS records into its own local store, a message left on TAS-A is, by default, invisible to TAS-B. Clustering makes a subscriber's mailbox behave as one logical mailbox across every TAS, so the waiting count reflects messages left on any node and retrieval from any node can list, play, and delete every message.
This is achieved without Erlang clustering, distributed Mnesia, or a shared/central database.
Each TAS keeps its own local store; nodes simply ask each other over HTTP when they need the full
picture. Clustering is opt-in β when the cluster config key is absent the TAS behaves exactly
as a single node.
Partitioned ownership, scatter-gather readsβ
Each voicemail stays where it was recorded. The node that took the deposit is the message's owning node and keeps both the metadata row and the recording WAV. Nothing is replicated and there is no schema change β ownership is simply whichever node returns the row. When a node needs the full view of a mailbox it queries its own store and fans out an HTTP request to every other TAS, then merges the results. Because each message has exactly one owner, marking-read and delete always route back to that single owner, so there are no distributed write conflicts.
Node discoveryβ
A TAS finds its peers in one of two ways (set by cluster.discovery); in both cases it excludes
itself so it never queries itself over HTTP.
- Static (
:static) β the full member list is configured inruntime.exsand deployed identically to every node. Each node identifies itself withself_idand drops that entry. Predictable and dependency-free; adding/removing a node means editing config everywhere. - DNS (
:dns) β the node resolves a DNS name that returns every TAS address (A/AAAA records, one per TAS) and builds a peer URL per address, dropping its own. A short DNS TTL keeps the peer set current without redeploying config; an SRV record can carry the port per target.
Either way the peer set is just a list of base URLs; everything downstream is identical.
Getting the count (fan-out)β
The waiting-message count is computed by fanning out to every other TAS and summing β it runs on every deposit because the MWI text depends on it.
Fan-out runs concurrently with a short per-request timeout, so a slow or unreachable peer never blocks the deposit flow.
Retrieval from any node (media streaming)β
On retrieval the handling node builds the merged mailbox β its own messages plus every peer's β
sorted oldest-first, each message tagged with its owning node. To play a remote message it
streams the WAV from the owner via GET /api/voicemail/media, writes it to a temporary file under
its local storage_dir (so the co-located FreeSWITCH can read it), plays it, then removes the temp
file. Recordings are fetched lazily, only when a message is about to play, so a subscriber who
hangs up early triggers no further transfers. Marking-read and delete are routed to the owning node
(POST /api/voicemail/mark_read / .../delete).
Inter-TAS HTTP APIβ
Fan-out runs over the existing TAS HTTP listener. The read/single-write endpoints operate on the node's own local store only β the cluster-wide view is assembled by the calling node.
| Method & Path | Purpose |
|---|---|
GET /api/voicemail/count?mailbox=<msisdn> | Unread count in this node's store. |
GET /api/voicemail/messages?mailbox=<msisdn> | This node's message rows for the mailbox. |
GET /api/voicemail/media?mailbox=<msisdn>&uuid=<uuid> | Streams the recording WAV held by this node. |
GET /api/voicemail/greeting?mailbox=<msisdn> | Streams this node's personal greeting for the mailbox, if any. |
POST /api/voicemail/mark_read {mailbox, uuid} | Marks one message read in this node's store. |
POST /api/voicemail/delete {mailbox, uuid} | Deletes one message (row + recording) from this node. |
DELETE /api/voicemail/greeting?mailbox=<msisdn> | Clears the personal greeting (reverts to default). Cluster-aware β see management. |
DELETE /api/voicemail/mailbox?mailbox=<msisdn> | Deletes all messages (rows + recordings) for the mailbox. Cluster-aware β see management. |
The GET/POST endpoints above act on the local store only β the cluster-wide view is assembled
by the calling node. The two DELETE management actions instead fan out to peers; the peer leg
carries a ?scope=local flag so a fanned-out request is applied locally and not re-fanned,
preventing recursion.
Failure handling (best-effort, never silent)β
A single unreachable peer must not break voicemail for everyone:
- Counting β a peer that times out contributes
0. This can briefly under-count, but every such failure is logged and recorded as a metric; it is never silently swallowed. - Retrieval β messages from an unreachable peer are omitted (logged); a media fetch that fails mid-session is logged and skipped rather than dropping the call.
- A recovering peer simply rejoins the next fan-out β there is no re-sync step, because nothing was ever replicated.
Securityβ
Inter-TAS requests carry voicemail content, so the cluster endpoints require a shared secret header (verified by the receiving node) and optional source-IP allow-listing (the same allow-list pattern as other TAS interfaces). Self-signed certificates between nodes are accepted as for the SMSc integration.
Management API: Clearing Greetings and Mailboxesβ
Besides the per-node primitives above, two management actions let an operator (or self-care
front-end) reset a subscriber's voicemail over HTTP. Both are cluster-aware: because a greeting
or a message can live on any node, the request fans out to every TAS and applies the action on
each node that holds the relevant data. On a single node (no cluster configured) the action simply
applies locally.
Clearing a greetingβ
Removes a subscriber's personal greeting so callers revert to the default deposit_greeting.
DELETE /api/voicemail/greeting?mailbox=<msisdn>
The receiving node deletes its own greeting.wav for the mailbox (a no-op if it holds none) and, in
a cluster, fans the same DELETE out to peers so a greeting recorded on any node is removed. Returns
success once every reachable node has applied it; an unreachable node is reported so the caller knows
the clear was partial.
Clearing a mailboxβ
Deletes all messages (rows and recordings) for a mailbox β e.g. when deprovisioning a subscriber or on operator request.
DELETE /api/voicemail/mailbox?mailbox=<msisdn>
The receiving node deletes every message it owns for the mailbox and fans the DELETE out to peers,
so messages held on any node are removed. Because this clears the last unread message everywhere, the
MWI is cleared for the mailbox. As with greeting clears, a partial
result is reported if any node is unreachable.
These management endpoints are protected by the same shared secret (and optional source-IP allow-list) as the rest of the inter-TAS API.
Configurationβ
All voicemail settings live under the :tas application's voicemail key in runtime.exs.
outbound_socket, record, and menu are optional β they default in code and are shown here
commented out. The speech prompts are TTS-generated by the shared :tas, :prompts recordings block
(see Prompt files); the prompts map here just points at the resulting paths.
config :tas,
voicemail: %{
timezone: "Pacific/Tahiti", # Timezone used in timestamps
say_language: "fr", # Fallback language for the spoken date/time
# (channel default_language wins per call)
storage_dir: "/usr/local/freeswitch/storage/voicemail", # Where recordings are written
# Optional β defaults shown; omit to use them:
# outbound_socket: %{listen_ip: "127.0.0.1", listen_port: 8084},
# record: %{max_seconds: 120, silence_threshold: 200, silence_seconds: 5, min_seconds: 2},
# menu: %{tries: 3, timeout_ms: 5000, terminators: "#"},
# Prompt files FreeSWITCH plays (ABSOLUTE paths; $${base_dir} is NOT expanded over ESL).
# The speech prompts are generated by the :tas, :prompts recordings block; `beep` is a tone.
prompts: %{
deposit_greeting: "/usr/local/freeswitch/sounds/tas/vm/deposit_greeting.wav",
beep: "tone_stream://%(500,0,800)",
retrieve_received_at: "/usr/local/freeswitch/sounds/tas/vm/received_at.wav",
retrieve_menu: "/usr/local/freeswitch/sounds/tas/vm/menu.wav",
retrieve_no_messages: "/usr/local/freeswitch/sounds/tas/vm/no_messages.wav",
retrieve_deleted: "/usr/local/freeswitch/sounds/tas/vm/deleted.wav",
retrieve_saved: "/usr/local/freeswitch/sounds/tas/vm/saved.wav",
retrieve_goodbye: "/usr/local/freeswitch/sounds/tas/vm/goodbye.wav",
# Personal greetings (only needed when greetings.enabled)
greeting_record: "/usr/local/freeswitch/sounds/tas/vm/greeting_record.wav",
greeting_saved: "/usr/local/freeswitch/sounds/tas/vm/greeting_saved.wav",
# Forwarding (only needed when forwarding.enabled)
forward_enter_mailbox: "/usr/local/freeswitch/sounds/tas/vm/forward_enter_mailbox.wav",
forward_done: "/usr/local/freeswitch/sounds/tas/vm/forward_done.wav",
forward_invalid: "/usr/local/freeswitch/sounds/tas/vm/forward_invalid.wav"
},
# Optional β per-subscriber greetings. Omit (or enabled: false) to always use deposit_greeting.
# greetings: %{enabled: true},
# Optional β forward a message to another mailbox from the retrieval menu (key 4).
# forwarding: %{enabled: true, menu_key: "4"},
# Optional β auto-expire old messages. Omit to keep messages until manually deleted.
# expiry: %{
# enabled: true,
# max_age_days: 30, # delete messages older than this
# read_max_age_days: 7, # optional: purge already-read messages sooner
# sweep_interval_minutes: 60
# },
# Optional β multi-TAS clustering. Omit entirely to run single-node (no fan-out).
# cluster: %{
# discovery: :static, # :static (nodes list) or :dns (dns_name)
# self_id: "tas-a",
# nodes: [
# %{id: "tas-a", base_url: "https://10.8.82.60:8080"},
# %{id: "tas-b", base_url: "https://10.8.82.61:8080"}
# ],
# # discovery: :dns, dns_name: "omnitas-vm.internal.example.com", scheme: "https", port: 8080,
# shared_secret: System.get_env("VM_CLUSTER_SECRET"),
# request_timeout_ms: 1500
# },
smsc: %{
smsc_url: "https://10.80.14.219:8443", # SMSc / Omnimessage API base URL
source_msisdn: "2222" # Sender for notification messages
},
# For usage of variables in this section see the MWI table above.
voicemail_notification_text: %{
not_left:
"Vous avez 1 appel manquΓ© du <%= caller %> le <%= day %>/<%= month %> Γ <%= hour %>:<%= minute %>",
single_voicemail:
"Vous avez un nouveau message vocal du <%= caller %> le <%= day %>/<%= month %> Γ <%= hour %>:<%= minute %>. Pour le consulter, composez le 2222.",
multiple_voicemails:
"Vous avez <%= message_count %> nouveaux messages vocaux. Pour les consulter, composez le 2222."
}
}
Parametersβ
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
timezone | String | No | "Etc/UTC" | IANA timezone for date/time substituted into notification text. |
outbound_socket.listen_ip | String | No | "127.0.0.1" | IP the voicemail listener binds to. Loopback when co-located. Must match the dialplan socket action. |
outbound_socket.listen_port | Integer | No | 8084 | TCP port the voicemail listener binds to. Must match the dialplan socket action. |
say_language | String | No | "en" | Fallback language code passed to FreeSWITCH say for the spoken message date/time. Used only when the channel's default_language is not set; default_language takes precedence per call. |
storage_dir | String | Yes | - | Directory where recordings are written. Must be writable by FreeSWITCH and readable by the TAS. Created on demand per mailbox. |
record.max_seconds | Integer | No | 120 | Maximum message length in seconds. |
record.silence_threshold | Integer | No | 200 | Energy level below which audio is treated as silence. |
record.silence_seconds | Integer | No | 5 | Seconds of continuous silence that end the recording. |
record.min_seconds | Integer | No | 2 | Recordings shorter than this are discarded and treated as a missed call. |
menu.tries | Integer | No | 3 | Times the menu prompt replays while waiting for a valid digit. |
menu.timeout_ms | Integer | No | 5000 | Milliseconds to wait for a digit on each try. |
menu.terminators | String | No | "#" | DTMF key(s) that terminate digit entry. |
prompts.* | String | Yes | - | Absolute paths to prompt audio files (see table below). |
smsc.smsc_url | String | Yes | - | Base URL of the SMSc / Omnimessage API. The /api/mwi endpoint is appended automatically. |
smsc.source_msisdn | String | Yes | - | Sender address shown on notification messages. |
voicemail_notification_text.not_left | String | Yes | - | Body sent when a call reached voicemail but no message was left. |
voicemail_notification_text.single_voicemail | String | Yes | - | Body sent when exactly one unread message is waiting. |
voicemail_notification_text.multiple_voicemails | String | Yes | - | Body sent when more than one unread message is waiting. |
voicemail_notification_text.cleared | String | No | (default) | Optional body used when clearing the indicator after retrieval. A non-empty body is required for the SMSc to build a deliverable message. |
greetings.enabled | Boolean | No | false | Allow subscribers to record a personal greeting (played to callers instead of deposit_greeting). |
forwarding.enabled | Boolean | No | false | Offer "forward to another mailbox" in the retrieval menu. |
forwarding.menu_key | String | No | "4" | DTMF key that triggers forwarding during retrieval. |
expiry.enabled | Boolean | No | false | Enable the periodic sweeper that deletes aged messages. |
expiry.max_age_days | Integer | No | - | Delete messages older than this many days. Required when expiry.enabled. |
expiry.read_max_age_days | Integer | No | (unset) | If set, already-read messages are purged after this many days (shorter retention than unread). |
expiry.sweep_interval_minutes | Integer | No | 60 | How often the expiry sweeper runs. |
cluster | Map | No | (unset) | Enables multi-TAS voicemail. When unset, the node is single-node (no fan-out). |
cluster.discovery | Atom | Yes (if cluster set) | - | :static for a configured node list, :dns for DNS-based discovery. |
cluster.self_id | String | Yes (:static) | - | This node's id; its entry is excluded from the peer set. |
cluster.nodes | List of maps | Yes (:static) | - | Cluster members, each %{id, base_url}. Deploy the same list to every node. |
cluster.dns_name | String | Yes (:dns) | - | DNS name resolving to every TAS address. The node excludes its own address. |
cluster.scheme | String | No (:dns) | "https" | URL scheme used to build peer base URLs. |
cluster.port | Integer | No (:dns) | 8080 | Port used to build peer base URLs (ignored when SRV records carry the port). |
cluster.shared_secret | String | Yes (if cluster set) | - | Secret presented and verified on inter-TAS requests. |
cluster.request_timeout_ms | Integer | No | 1500 | Per-request fan-out timeout. A peer exceeding it contributes nothing (logged). |
Prompt filesβ
Prompt values are absolute paths to audio files readable by FreeSWITCH (the $${base_dir}
shortcut is not expanded for files played over ESL), or a tone_stream:// URI for tones.
The speech prompts are generated at boot by the shared :tas, :prompts recordings block (the
same OpenAI TTS step used for the credit/announcement prompts). Add a recording there with the
text and a base-relative path, then point the matching voicemail.prompts entry at the absolute
path (the generator writes under /usr/local/freeswitch). Only missing files are generated.
| Parameter | Description |
|---|---|
deposit_greeting | Played to the caller before the beep during deposit. |
beep | Played before recording, and before each message during retrieval. A tone_stream:// URI (e.g. tone_stream://%(500,0,800)) generates the tone with no file. |
retrieve_received_at | Fixed "voicemail received at" lead-in, followed by the spoken date/time. |
retrieve_menu | The options menu, e.g. "to hear this again press 1, to delete press 2, to save press 3". |
retrieve_no_messages | Played when the mailbox is empty. |
retrieve_deleted | Confirmation played after a message is deleted. |
retrieve_saved | Confirmation played after a message is saved. |
retrieve_goodbye | Played at the end of the retrieval session. |
greeting_record | Prompt to record a personal greeting (only when greetings.enabled). |
greeting_saved | Confirmation after a personal greeting is saved. |
forward_enter_mailbox | Prompt to key in the target mailbox MSISDN (only when forwarding.enabled). |
forward_done | Confirmation after a message is forwarded. |
forward_invalid | Played when the forward target mailbox is unknown/invalid. |
Troubleshootingβ
Calls to voicemail drop immediatelyβ
Symptoms: A call routed to a voicemail extension hangs up before the greeting or menu.
Possible causes:
- The dialplan
socketaddress/port does not matchoutbound_socket. - The voicemail listener is not running.
- A firewall is blocking the loopback port (unusual when co-located).
Resolution:
- Confirm the dialplan
socketaction uses the same IP and port asoutbound_socket. - Verify the TAS is running and the voicemail listener started.
- Confirm FreeSWITCH can reach
listen_ip:listen_port.
Messages not stored or not visible in the UIβ
Symptoms: A message is left but does not appear in the UI/REST API, or vm_boxcount returns 0.
Possible causes:
mod_voicemailis not loaded, so thevoicemail_msgstable /vm_boxcountare unavailable.- The recording was shorter than
record.min_secondsand was treated as a missed call. storage_diris not writable by FreeSWITCH.
Resolution:
- Ensure
mod_voicemailremains loaded in FreeSWITCH. - Check the recording length against
record.min_seconds. - Verify
storage_dirpermissions.
No prompts are heardβ
Symptoms: Silence where a greeting, beep, or menu should play.
Possible causes:
- A
promptspath is wrong or not readable by FreeSWITCH. - A
promptspath used$${base_dir}, which is not expanded over ESL.
Resolution:
- Confirm each
promptsvalue is an absolute path to a file FreeSWITCH can read. - Replace any
$${base_dir}-style paths with absolute paths.
The message-waiting indicator does not clearβ
Symptoms: After listening to all messages, the handset still shows messages waiting.
Possible causes:
- The SMSc rejected the clear because the message body was empty.
- Unread messages remain (the session ended before all messages were heard).
Resolution:
- Ensure a
cleared(or default) notification body is configured. - Confirm the subscriber listened to all messages; only played messages are marked read.
Count is too low / messages missing across a clusterβ
Symptoms: A subscriber's badge shows fewer messages than were left, or messages left on another TAS are not heard during retrieval.
Possible causes:
- A peer TAS is down or unreachable, so it contributed nothing to the fan-out (logged + metric).
- The
cluster.shared_secretdoes not match between nodes, so peers reject the requests. - Discovery is misconfigured (wrong
nodeslist, ordns_namenot returning all addresses). - A firewall blocks the HTTP listener port between TAS nodes.
Resolution:
- Check the cluster fan-out error logs/metrics to see which peer failed.
- Confirm
shared_secretis identical on every node and discovery returns every node. - Verify each node can reach every other node's HTTP listener port.
A personal greeting is not playedβ
Symptoms: Callers hear the default greeting even though the subscriber recorded their own.
Possible causes:
greetings.enabledis not set.- In a cluster, the greeting was recorded on a different node and that node was unreachable during the deposit's greeting lookup.
Resolution:
- Confirm
greetings.enabledis true and the greeting was saved (confirmation prompt heard). - Confirm the node that recorded the greeting is reachable from the depositing node.
Old messages are not being removedβ
Symptoms: Mailboxes keep messages indefinitely / disk usage grows.
Possible causes:
expiry.enabledis not set, ormax_age_daysis unset.- The sweep interval has not elapsed yet.
Resolution:
- Confirm
expiry.enabledis true andmax_age_daysis configured. - Allow up to
sweep_interval_minutesfor the next sweep, then check the logs.