Outbound DIALSTATUS Handling
The Problem
The default Asterisk dialplanThe core call-routing configuration of Asterisk, written mostly in extensions.conf as contexts, extensions, and priorities that decide how every call is handled. Full definition → pattern for outbound call failures is to play "all circuits are busy now" for any failure. This is misleading, a bad phone number is not a trunkA connection between Asterisk and another PBX or an ITSP/carrier, used to send and receive external calls. failure, but the user hears the same message for both.
DIALSTATUS vs HANGUPCAUSE
After Dial() returns, Asterisk sets two variables:
- DIALSTATUS: High-level result (ANSWER, BUSY, CONGESTION, CHANUNAVAIL, etc.)
- HANGUPCAUSE: The Q.850/ISUP cause code from the network, giving the real reason
DIALSTATUS alone cannot distinguish between "the number you dialed does not
exist" and "the trunk is down." Both can return CONGESTION or CHANUNAVAIL.
Checking HANGUPCAUSE gives you the actual network-level reason.
Common Q.850 Cause Codes
| Code | Name | Meaning |
|---|---|---|
| 1 | UNALLOCATED_NUMBER | Number does not exist |
| 16 | NORMAL_CLEARING | Normal call completion |
| 17 | USER_BUSY | Destination busy |
| 19 | NO_USER_RESPONSE | No answer |
| 28 | INVALID_NUMBER_FORMAT | Bad number format |
| 34 | NO_CIRCUIT_AVAILABLE | Actual network congestion |
| 38 | NETWORK_OUT_OF_ORDER | Trunk/network failure |
| 127 | INTERWORKING | Unspecified interworking error |
The Fix
Outbound Status Subroutine
Call this after every Dial() to the trunk:
[outbound-status]
; Answered calls - normal completion
exten => ANSWER,1,NoOp(Call answered normally)
same => n,Return()
; Busy signal
exten => BUSY,1,NoOp(Destination busy)
same => n,Playback(number-busy)
same => n,Return()
; No answer
exten => NOANSWER,1,NoOp(No answer)
same => n,Return()
; Caller cancelled
exten => CANCEL,1,NoOp(Caller cancelled)
same => n,Return()
; Congestion - check cause code for specific failure reason
exten => CONGESTION,1,NoOp(Congestion - HANGUPCAUSE=${HANGUPCAUSE})
same => n,GotoIf($["${HANGUPCAUSE}" = "1"]?unallocated)
same => n,GotoIf($["${HANGUPCAUSE}" = "28"]?invalid_number)
same => n,Playback(all-circuits-busy-now)
same => n,Playback(pls-try-call-later)
same => n,Return()
same => n(unallocated),Playback(ss-noservice)
same => n,Return()
same => n(invalid_number),Playback(ss-noservice)
same => n,Return()
; Channel unavailable - check if trunk issue or bad number
exten => CHANUNAVAIL,1,NoOp(Channel unavailable - HANGUPCAUSE=${HANGUPCAUSE})
; Cause 1 = unallocated number, not a trunk issue
same => n,GotoIf($["${HANGUPCAUSE}" = "1"]?bad_number)
; Cause 28 = invalid number format
same => n,GotoIf($["${HANGUPCAUSE}" = "28"]?bad_number)
; Cause 34 = no circuit available (actual congestion)
same => n,GotoIf($["${HANGUPCAUSE}" = "34"]?congestion)
; Anything else - assume trunk issue, alert ops
same => n,Playback(all-circuits-busy-now)
same => n,Playback(pls-try-call-later)
same => n,Set(SAFE_NUM=${FILTER(0-9+,${CALLERID(num)})})
same => n,Set(SLACK_MSG={"text":"*Outbound call failed* (cause=${HANGUPCAUSE})\nCaller: ${SAFE_NUM}\nEndpoint: ${CHANNEL(endpoint)}"})
same => n,System(curl -s -X POST -H 'Content-type: application/json' --data '${SLACK_MSG}' ${SLACK_WEBHOOK} &)
same => n,Return()
same => n(bad_number),Playback(ss-noservice)
same => n,Return()
same => n(congestion),Playback(all-circuits-busy-now)
same => n,Playback(pls-try-call-later)
same => n,Return()
; Invalid arguments
exten => INVALIDARGS,1,NoOp(Invalid dial arguments)
same => n,Playback(ss-noservice)
same => n,Return()
; Catch-all for any other status
exten => _X.,1,NoOp(Unknown dial status: ${EXTEN})
same => n,Return()
Usage in Outbound Context
[outbound]
exten => _NXXNXXXXXX,1,NoOp(Outbound to ${EXTEN})
same => n,Gosub(set-outbound-cid,s,1)
same => n,Dial(PJSIP/1${EXTEN}@${TRUNK},120,tT)
same => n,Gosub(outbound-status,${DIALSTATUS},1)
same => n,Hangup()
What the User Hears
| Scenario | Old Behavior | New Behavior |
|---|---|---|
| Bad number (cause 1) | "All circuits are busy now" | "ss-noservice" tone |
| Invalid format (cause 28) | "All circuits are busy now" | "ss-noservice" tone |
| Actual trunk failure | "All circuits are busy now" | "All circuits are busy now" + Slack alert |
| Network congestion (cause 34) | "All circuits are busy now" | "All circuits are busy now" |
Security Note
The CALLERID(num) is sanitized through FILTER(0-9+,...) before being passed
to System() via curl. This prevents command injection through crafted caller
ID values. Always sanitize any channelA single call leg passing through Asterisk. Channels represent connections to endpoints and are what dialplan applications act on. variable before using it in System().
Slack Integration
The Slack alert only fires for unknown/trunk-level failures, not for bad
numbers. This prevents alert fatigue from users misdialing while still catching
real trunk outages. The webhook URL is set in [globals]:
[globals]
SLACK_WEBHOOK = https://hooks.slack.com/services/XXX/YYY/ZZZ
User Notes
Know a tip or gotcha for this topic? Share it below and help others.
Contribute a note
Share a tip, gotcha, or practical example. Keep it under 2000 characters. No questions (use the Asterisk community forums for support). Wrap code in backticks.