Skip to main content

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.

Core Documentation​

Call Processing Integration​

Monitoring​


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_voicemail database schema, so vm_boxcount, the web UI, and the REST API keep working without modification. mod_voicemail must remain loaded in FreeSWITCH so the database and the vm_boxcount command 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.

VariableSet whenDescription
tas_vm_modeAlwaysdeposit to leave a message, retrieve to listen to messages, or greeting to record a personal greeting (see Personal Greetings). Selects the flow.
tas_vm_mailboxAlwaysMailbox owner's MSISDN. For deposit, the called subscriber; for retrieval, the calling subscriber (the box owner).
tas_vm_callerDepositCalling party's number, stored with the message and used in the notification text.
default_languageOptionalLanguage 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​

  1. Answer the call.
  2. Play the record-greeting prompt (prompts.greeting_record).
  3. Play the beep (prompts.beep) and record, ending on the terminator key, silence, or the maximum length (the same record limits as a deposit).
  4. 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 as greeting.wav under the mailbox's directory in storage_dir, and prompts.greeting_saved is 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​

  1. Answer the call.
  2. Play the greeting. If the subscriber has recorded a personal greeting it is played; otherwise the default prompts.deposit_greeting is used.
  3. Play the beep (prompts.beep).
  4. Record to storage_dir, ending on caller hangup, the terminator key, silence, or the maximum length.
  5. 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.
  6. 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:

KeyActionEffect
1Hear againReplays the current message from the top.
2DeleteRemoves the message and its recording, plays the "deleted" prompt, advances.
3SaveKeeps the message (already marked read), plays the "saved" prompt, advances.
4ForwardForwards 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).

EventIndicationNotification text used
Deposit, no message leftInactive (missed call)voicemail_notification_text.not_left
Deposit, one message waitingActivevoicemail_notification_text.single_voicemail
Deposit, multiple messages waitingActivevoicemail_notification_text.multiple_voicemails
Retrieval finishedClearedvoicemail_notification_text.cleared (or a default)

Variables available in the notification templates:

VariableAvailable inDescription
callerAllCalling party number that left the message / missed call.
dayAllDay of month, in the configured timezone.
monthAllMonth number, in the configured timezone.
hourAllHour (24h), in the configured timezone.
minuteAllMinute, zero-padded, in the configured timezone.
message_countmultiple_voicemailsNumber 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.db SQLite database, in the standard voicemail_msgs table.

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.

  1. The subscriber presses 4 and is prompted (prompts.forward_enter_mailbox) to key in the target mailbox MSISDN, terminated with #.
  2. The current recording is copied into the target mailbox as a new, unread message. The original caller details are preserved and the forwarded_by column records the forwarding subscriber, so the target sees who forwarded it.
  3. An MWI notification fires to the target mailbox exactly as a normal deposit would.
  4. The subscriber hears a confirmation (prompts.forward_done); an unknown/invalid target plays prompts.forward_invalid and 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) exceeds expiry.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_msgs row 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 in runtime.exs and deployed identically to every node. Each node identifies itself with self_id and 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 & PathPurpose
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​

ParameterTypeRequiredDefaultDescription
timezoneStringNo"Etc/UTC"IANA timezone for date/time substituted into notification text.
outbound_socket.listen_ipStringNo"127.0.0.1"IP the voicemail listener binds to. Loopback when co-located. Must match the dialplan socket action.
outbound_socket.listen_portIntegerNo8084TCP port the voicemail listener binds to. Must match the dialplan socket action.
say_languageStringNo"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_dirStringYes-Directory where recordings are written. Must be writable by FreeSWITCH and readable by the TAS. Created on demand per mailbox.
record.max_secondsIntegerNo120Maximum message length in seconds.
record.silence_thresholdIntegerNo200Energy level below which audio is treated as silence.
record.silence_secondsIntegerNo5Seconds of continuous silence that end the recording.
record.min_secondsIntegerNo2Recordings shorter than this are discarded and treated as a missed call.
menu.triesIntegerNo3Times the menu prompt replays while waiting for a valid digit.
menu.timeout_msIntegerNo5000Milliseconds to wait for a digit on each try.
menu.terminatorsStringNo"#"DTMF key(s) that terminate digit entry.
prompts.*StringYes-Absolute paths to prompt audio files (see table below).
smsc.smsc_urlStringYes-Base URL of the SMSc / Omnimessage API. The /api/mwi endpoint is appended automatically.
smsc.source_msisdnStringYes-Sender address shown on notification messages.
voicemail_notification_text.not_leftStringYes-Body sent when a call reached voicemail but no message was left.
voicemail_notification_text.single_voicemailStringYes-Body sent when exactly one unread message is waiting.
voicemail_notification_text.multiple_voicemailsStringYes-Body sent when more than one unread message is waiting.
voicemail_notification_text.clearedStringNo(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.enabledBooleanNofalseAllow subscribers to record a personal greeting (played to callers instead of deposit_greeting).
forwarding.enabledBooleanNofalseOffer "forward to another mailbox" in the retrieval menu.
forwarding.menu_keyStringNo"4"DTMF key that triggers forwarding during retrieval.
expiry.enabledBooleanNofalseEnable the periodic sweeper that deletes aged messages.
expiry.max_age_daysIntegerNo-Delete messages older than this many days. Required when expiry.enabled.
expiry.read_max_age_daysIntegerNo(unset)If set, already-read messages are purged after this many days (shorter retention than unread).
expiry.sweep_interval_minutesIntegerNo60How often the expiry sweeper runs.
clusterMapNo(unset)Enables multi-TAS voicemail. When unset, the node is single-node (no fan-out).
cluster.discoveryAtomYes (if cluster set)-:static for a configured node list, :dns for DNS-based discovery.
cluster.self_idStringYes (:static)-This node's id; its entry is excluded from the peer set.
cluster.nodesList of mapsYes (:static)-Cluster members, each %{id, base_url}. Deploy the same list to every node.
cluster.dns_nameStringYes (:dns)-DNS name resolving to every TAS address. The node excludes its own address.
cluster.schemeStringNo (:dns)"https"URL scheme used to build peer base URLs.
cluster.portIntegerNo (:dns)8080Port used to build peer base URLs (ignored when SRV records carry the port).
cluster.shared_secretStringYes (if cluster set)-Secret presented and verified on inter-TAS requests.
cluster.request_timeout_msIntegerNo1500Per-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.

ParameterDescription
deposit_greetingPlayed to the caller before the beep during deposit.
beepPlayed 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_atFixed "voicemail received at" lead-in, followed by the spoken date/time.
retrieve_menuThe options menu, e.g. "to hear this again press 1, to delete press 2, to save press 3".
retrieve_no_messagesPlayed when the mailbox is empty.
retrieve_deletedConfirmation played after a message is deleted.
retrieve_savedConfirmation played after a message is saved.
retrieve_goodbyePlayed at the end of the retrieval session.
greeting_recordPrompt to record a personal greeting (only when greetings.enabled).
greeting_savedConfirmation after a personal greeting is saved.
forward_enter_mailboxPrompt to key in the target mailbox MSISDN (only when forwarding.enabled).
forward_doneConfirmation after a message is forwarded.
forward_invalidPlayed 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 socket address/port does not match outbound_socket.
  • The voicemail listener is not running.
  • A firewall is blocking the loopback port (unusual when co-located).

Resolution:

  1. Confirm the dialplan socket action uses the same IP and port as outbound_socket.
  2. Verify the TAS is running and the voicemail listener started.
  3. 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_voicemail is not loaded, so the voicemail_msgs table / vm_boxcount are unavailable.
  • The recording was shorter than record.min_seconds and was treated as a missed call.
  • storage_dir is not writable by FreeSWITCH.

Resolution:

  1. Ensure mod_voicemail remains loaded in FreeSWITCH.
  2. Check the recording length against record.min_seconds.
  3. Verify storage_dir permissions.

No prompts are heard​

Symptoms: Silence where a greeting, beep, or menu should play.

Possible causes:

  • A prompts path is wrong or not readable by FreeSWITCH.
  • A prompts path used $${base_dir}, which is not expanded over ESL.

Resolution:

  1. Confirm each prompts value is an absolute path to a file FreeSWITCH can read.
  2. 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:

  1. Ensure a cleared (or default) notification body is configured.
  2. 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_secret does not match between nodes, so peers reject the requests.
  • Discovery is misconfigured (wrong nodes list, or dns_name not returning all addresses).
  • A firewall blocks the HTTP listener port between TAS nodes.

Resolution:

  1. Check the cluster fan-out error logs/metrics to see which peer failed.
  2. Confirm shared_secret is identical on every node and discovery returns every node.
  3. 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.enabled is 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:

  1. Confirm greetings.enabled is true and the greeting was saved (confirmation prompt heard).
  2. 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.enabled is not set, or max_age_days is unset.
  • The sweep interval has not elapsed yet.

Resolution:

  1. Confirm expiry.enabled is true and max_age_days is configured.
  2. Allow up to sweep_interval_minutes for the next sweep, then check the logs.