Outbound DIALSTATUS Handling
The Problem
The default Asterisk dialplan 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 trunk 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 channel 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
No notes yet. Be the first to contribute a tip or example.
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.