Trunks & Queues

August 21, 2026 · View on GitHub

Trunks ([proxy.trunks])

Gateways to external SIP providers. Configured in [proxy.trunks] map or separate files.

[proxy.trunks.provider_a]
dest = "sip:sip.provider.com:5060"
# Optional failover
backup_dest = "sip:backup.provider.com"

# Authentication
username = "myuser"
password = "mypassword"

# Capacity
max_calls = 50
max_cps = 5          # Calls per second
weight = 10          # Relative weight for load balancing

# Traffic Control
direction = "outbound"       # inbound, outbound, bidirectional
inbound_hosts = ["203.0.113.50"] # Whitelist IPs

Trunk Fields

FieldTypeDefaultDescription
deststringrequiredSIP URI of the gateway
backup_deststringnoneFailover SIP URI
username / passwordstringnoneSIP authentication credentials
codec[string][]Allowed codecs (alias: allow_codecs, audio_codecs)
transportstringnoneTransport protocol override (e.g., "tcp")
max_callsintnoneMax concurrent calls
max_cpsintnoneMax calls per second
weightintnoneLoad balancing weight
directionstring"bidirectional"inbound, outbound, bidirectional
inbound_hosts[string][]Source IP whitelist for inbound calls
disabledboolfalseDisable trunk without removing it
countrystringnoneCountry code for number normalization
did_numbers[string][]DID numbers owned by this trunk (inbound routing)
call_id_modestringnoneCall-ID rewriting: "prefix", "suffix", "none"
rewrite_hostportbooltrueRewrite host:port in outgoing Contact headers
recordingtablenonePer-trunk recording policy override
ringbacktablenonePer-trunk ringback audio override
max_ring_timeintnonePer-trunk max ring/setup time (seconds) before a no-answer call is rejected with 408. 0 disables the ring timeout for this trunk. Overrides the global [proxy] max_ring_time for calls routed through this trunk
external_ipstringnoneOverride the IP advertised in SDP c=/o= lines and ICE candidates for this trunk's legs. Replaces the global rtp_config.external_ip. Essential when some trunks terminate on an overlay network (Tailscale/WireGuard) that needs a different advertised IP than the public NAT address
bind_ipstringnoneOverride the local IP RTP sockets bind to for this trunk's legs. Replaces the global rtp_config.bind_ip
header_passthroughtablenoneControl which custom headers from the original INVITE are forwarded to this trunk's outbound INVITE. mode is "all" (default), "whitelist", or "blacklist"; whitelist/blacklist are header-name lists (case-insensitive). Standard SIP headers (Via/From/To/Call-ID/CSeq/Contact/...) are never forwarded. Unset (default) = forward nothing to external trunks; internal destinations (same realm / registered / home-proxy) always forward everything unless overridden by the route's with_original_headers

Trunk Registration

For trunks that require outbound registration:

[proxy.trunks.sip_provider]
dest = "sip:sip.provider.com:5060"
username = "myuser"
password = "mypassword"

# SIP registration (register at this trunk)
register_enabled = true
register_expires = 3600
# register_extra_headers = { "X-Client-ID" = "my-pbx" }

Trunk Health Checks

Optional health monitoring for trunk availability:

[proxy.trunks.provider_a]
dest = "sip:sip.provider.com:5060"

health_check_enabled = true
health_check_interval_secs = 30   # Probe every 30s
health_check_probe_count = 3      # Fail after 3 failed probes
health_check_fallback_trunk = "backup-provider"  # Auto-failover

Advanced Trunk Settings

[proxy.trunks.provider_a]
dest = "sip:sip.provider.com:5060"

# Call Admission Control
cac_policy = "loss_based"         # "loss_based" or "reject"
overflow_threshold = 90           # Trigger CAC at 90% capacity

# Media handling
media_mode = "auto"               # "auto", "none", "bypass", "force_transcode"
                                  # - auto: bridge only for app/queue flows
                                  # - none: no media proxy (SDP passthrough, RTP direct)
                                  # - bypass: SDP rewrite only, RTP direct
                                  # - force_transcode: always bridge through PBX
video_policy = "pass_through"      # "passthrough", "strip", "transcode"

# Per-trunk IP override (for overlay networks like Tailscale/WireGuard)
external_ip = "100.64.10.1"       # Override global rtp_config.external_ip
bind_ip = "100.64.10.2"           # Override global rtp_config.bind_ip

# See [06-media-recording.md](06-media-recording.md) for the full media proxy
# reference, including latching, trunk-level vs server-level configuration,
# and recommended combinations for NAT / overlay scenarios.

# SIP header manipulation
header_rules = [
    { action = "add", name = "X-Client-ID", value = "rustpbx" },
    { action = "remove", name = "X-Internal-Info" },
]

# Forward original custom headers to this trunk's outgoing INVITE.
# Unset (default) -> forward nothing; internal destinations forward everything.
header_passthrough = { mode = "all" }            # all custom headers
# header_passthrough = { mode = "whitelist", whitelist = ["X-Smart2Agent", "X-SmartParams"] }
# header_passthrough = { mode = "blacklist", blacklist = ["X-Token"] }

# Number normalization
incoming_from_user_prefix = ""    # Strip prefix from inbound caller
incoming_to_user_prefix = ""      # Strip prefix from inbound callee

Queues ([proxy.queues])

Call distribution logic (ACD).

[proxy.queues.support_main]
name = "General Support"
accept_immediately = true
passthrough_ringback = false
# acd_policy = "default"       # Reference to ACD policy (CC addon)

# Hold Music
[proxy.queues.support_main.hold]
audio_file = "sounds/hold_music.wav"
loop_playback = true

# Distribution Strategy
[proxy.queues.support_main.strategy]
mode = "sequential" # or "parallel" (ring-all)
wait_timeout_secs = 20

[[proxy.queues.support_main.strategy.targets]]
uri = "sip:1001@local"
label = "Alice"

[[proxy.queues.support_main.strategy.targets]]
uri = "sip:1002@local"
label = "Bob"

# Fallback (if no agents answer)
[proxy.queues.support_main.fallback]
action = "redirect" # or "hangup", "queue"
redirect = "sip:voicemail@local"
# queue_ref = "overflow_queue"

# Voice prompts (played to caller while waiting)
# [proxy.queues.support_main.voice_prompts]
# estimated_wait = "sounds/estimated_wait.wav"
# position = "sounds/position.wav"
# periodic = "sounds/thank_you.wav"
# periodic_interval_secs = 60

Queue Fields

FieldTypeDefaultDescription
namestringnoneDisplay name
acd_policystringnoneACD policy name (CC addon)
accept_immediatelyboolfalseCall is accepted (200 OK) before agent answers
passthrough_ringbackboolfalseForward callee ringback to caller
holdtablenoneHold music config
strategytablerequiredDistribution strategy (mode, targets, timeout)
fallbacktablenoneFallback when no agent answers
voice_promptstablenoneVoice announcements during wait

Queue Transfer Query Parameters

When transferring a call to a queue via queue:<name>, you can append query parameters to override queue configuration at runtime.

?return_ivr=<name> — Override the fallback action to transfer to an IVR instead of the configured fallback when no agents are available:

queue:support?return_ivr=main_menu

?target=<value> — Override the queue's configured agent targets with the given value. Supports skillgroup:<id> (resolved via AgentRegistry) or a SIP URI. Multiple &target= params are supported and dialed sequentially:

queue:support?target=skillgroup:sales                            # Single skill group
queue:support?target=sip:agent@pbx.com                           # Single SIP agent
queue:support?target=skillgroup:sales&target=skillgroup:support  # Multiple targets (sequential)

Combined usage:

queue:support?target=skillgroup:sales&return_ivr=main_menu

Queue Overflow Escalation (skill-group queues)

When a queue's dial target is a skill group (skill-group:{id}, via strategy.targets or ?target=skillgroup:...), the CC addon derives an escalation plan for it: the call is scheduled on the primary group only at first; after the configured queued-wait threshold the candidate set is widened to the overflow groups and ordered fairly (round-robin, the counter advances once per call) across the union — the new agents ring alongside the primary ones (cumulative mode).

Two configuration sources, in order of precedence:

1. Skill group overflow_groups + max_wait_secs (simplest — no ACD policy needed). Each overflow group becomes one escalation step at the group's max_wait_secs threshold, cumulative + fair:

# skill_groups TOML (or the /api/cc/skill-groups API — same fields)
[[skill_groups]]
skill_group_id = "support"
skills_required = ["support"]
overflow_groups = ["support_l2", "support_l3"]  # widen targets
max_wait_secs = 30                              # widen threshold (per queue wait)

2. ACD policy overflow.escalation_timeline (explicit, takes precedence when the group's acd_policy references a policy that defines one):

[policies.p1.overflow]
mode = "cumulative"          # cumulative = ring alongside; replace = redial

[[policies.p1.overflow.escalation_timeline]]
threshold_secs = 20
skill_group_id = "support_l2"
fair = true                  # widen with round-robin ordering (default false)
FieldTypeDefaultDescription
threshold_secsintrequiredQueued-wait seconds before this step triggers
skill_group_idstringrequiredGroup to widen to
fairboolfalseRound-robin ordering across the widened union
modestring"replace"cumulative (ring alongside) or replace (redial)