So, this my current version. I tested it as much as I could and so far I have not broke it. Thanks for all your help @lgaetz
Any feedback to improve it, especially performance-wise, would be much appreciated. My specific deployment will only have 15-20 users with likely only 2-4 calls at the same time. I am sure there are a lot of stuff that could be done better, this is my first time working with a PBX.
edit: in anonymizing my script from my working config i also left out important configuration lines. Should be complete now.
; FreePBX Custom Dialplan Logic for Advanced Outbound CID Management
;
; Purpose:
; This configuration dynamically sets outbound Caller IDs (CIDs) in a FreePBX system.
; It's designed for organizations with multiple teams, each needing different CIDs
; based on the dialed number's location (e.g., state, province) and country.
;
; Key Features:
; 1. Team-Based CIDs: Each team (like "SALES") has its own set of CIDs.
; 2. Location-Aware: Uses area codes to set region-specific CIDs.
; 3. Country-Specific: Differentiates between US and Canadian numbers.
; 4. Alternative CIDs: Use *31 prefix for "personal" or alternative CIDs.
; 5. Fallback System: Multiple fallback levels if a specific CID isn't found.
;
; How to Use:
; 1. Set each extension's ACCOUNTCODE to its team name (e.g., "SALES").
; 2. Configure team-specific CIDs in their sections (e.g., [SALES]).
; 3. Set outbound routes in FreePBX GUI to use this system.
; 4. If you intend to use *31 logic, ensure that for each dial pattern in your outbound route you have a *31 equivalent (*31 prefix AND append)
;
; Thanks to lgaetz for the support on this thread https://community.freepbx.org/t/multiple-teams-outbound-cid-by-country-area-code/96553/4
;
; Initial logic and area code stripping taken from lgaetz https://gist.github.com/lgaetz/2b3d67a30f86a827121b004ec2f3024a
;
; Usage: Content below goes in /etc/asterisk/extensions_custom.conf
;
; License: GNU GPL3+
[macro-dialout-trunk-predial-hook]
exten => s,1,NoOp(Entering Outbound CID Logic)
; Safety Checks: Skip this logic for emergency or inbound calls
exten => s,n,ExecIf($["${EMERGENCYROUTE}"="YES"]?MacroExit) ; Don't mess with emergency calls
exten => s,n,ExecIf($["${DIRECTION}"="INBOUND"]?MacroExit) ; Only handle outbound calls
; Set a global fallback CID in case all other methods fail
; This ensures we always have a valid CID to show
exten => s,n,Set(catchall_fallback_CID=<YOUR CATCHALL CID HERE>)
; Check if the caller's extension has an ACCOUNTCODE
; If it does, try to find a matching configuration section
; If not, or if no matching section exists, use the fallback CID
exten => s,n,GosubIf($[${EXISTS(${CHANNEL(accountcode)})}]?check_accountcode_exists:catchall_fallback_CID,s,1(${OUTNUM}))
exten => s,n(check_accountcode_exists),GosubIf($[${EXISTS(${CHANNEL(accountcode)},s,1)}]?${CHANNEL(accountcode)},s,1:catchall_fallback_CID,s,1(${OUTNUM}))
[<TEAM ACCOUNTCODE HERE>] ; Team 1 Configuration (e.g., [SALES])
; This section defines all the CIDs for a specific team.
; The name of this section must match an extension's ACCOUNTCODE.
exten => s,1,Set(Team_name=<TEAM ACCOUNTCODE HERE>) ; e.g., SALES
exten => s,n,NoOp(Entering Custom ${Team_name} Dialplan Logic)
; Default CIDs for this team
exten => s,n,Set(default_cid=<YOUR DEFAULT US CID HERE>) ; Used for US calls if no better match
exten => s,n,Set(default_cid_ca=<YOUR DEFAULT CA CID HERE>) ; Used for Canadian calls if no better match
; Alternative CIDs used when *31 is dialed
exten => s,n,Set(alternative_default_cid_us=<YOUR ALTERNATIVE US CID HERE>)
exten => s,n,Set(alternative_default_cid_ca=<YOUR ALTERNATIVE CA CID HERE>)
; Location-specific CIDs, e.g., for Ontario
exten => s,n,Set(outbound_cid_ON=<YOUR ONTARIO CID HERE>)
; You can add more location-specific CIDs here:
; exten => s,n,Set(outbound_cid_BC=<YOUR BRITISH COLUMBIA CID HERE>)
; exten => s,n,Set(outbound_cid_IL=<YOUR ILLINOIS CID HERE>)
; After setting all CIDs, go to the Dialing Engine
exten => s,n,Goto(Dialing_Engine,s,1)
; End of this team configuration. Repeat for each team.
[Dialing_Engine]
; This is the core logic that determines which CID to use.
; It's separate from team configurations to avoid code duplication.
exten => s,1,NoOp(Dialing Engine begins)
; Area Code Analysis
; Stripping Area Code from $OUTNUM and setting $AREA_MATCH to contain a 3 digit area code
; IMPORTANT: this is tested for US/CAN with international dial code "1". This may not behave as intended for countries with 2 digits country code. Adapt as needed.
exten => s,n,NoOp(Entering LOCATION_COUNTRY matching logic)
; exten => s,n,NoOp($OUTNUM is ${OUTNUM}) ; uncomment for testing
exten => s,n,ExecIf($["${OUTNUM:0:1}" = "+"]?Set(AREA_MATCH=${OUTNUM:1}):Set(AREA_MATCH=${OUTNUM})) ; Check if $OUTNUM contains a +. If it does strip it while setting AREA_MATCH
exten => s,n,ExecIf($[${LEN(${AREA_MATCH})}=10 || ${LEN(${AREA_MATCH})}=11]?set(AREA_MATCH=${AREA_MATCH:-10:3})) ; Handle 1. and . case for US/CAN by setting AREA_MATCH to the area code only
; Add below all desired LOCATION-COUNTRY combinations
; Canadian Area Codes
exten => s,n,ExecIf($["${AREA_MATCH}" = "204"]?Set(LOCATION_COUNTRY=MB-ca)) ; Manitoba
exten => s,n,ExecIf($["${AREA_MATCH}" = "226"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
exten => s,n,ExecIf($["${AREA_MATCH}" = "236"]?Set(LOCATION_COUNTRY=BC-ca)) ; British Columbia
exten => s,n,ExecIf($["${AREA_MATCH}" = "249"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
exten => s,n,ExecIf($["${AREA_MATCH}" = "250"]?Set(LOCATION_COUNTRY=BC-ca)) ; British Columbia
exten => s,n,ExecIf($["${AREA_MATCH}" = "289"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
exten => s,n,ExecIf($["${AREA_MATCH}" = "306"]?Set(LOCATION_COUNTRY=SK-ca)) ; Saskatchewan
exten => s,n,ExecIf($["${AREA_MATCH}" = "343"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
exten => s,n,ExecIf($["${AREA_MATCH}" = "365"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
exten => s,n,ExecIf($["${AREA_MATCH}" = "367"]?Set(LOCATION_COUNTRY=QC-ca)) ; Quebec
exten => s,n,ExecIf($["${AREA_MATCH}" = "387"]?Set(LOCATION_COUNTRY=AB-ca)) ; Alberta
exten => s,n,ExecIf($["${AREA_MATCH}" = "403"]?Set(LOCATION_COUNTRY=AB-ca)) ; Alberta
exten => s,n,ExecIf($["${AREA_MATCH}" = "416"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
exten => s,n,ExecIf($["${AREA_MATCH}" = "418"]?Set(LOCATION_COUNTRY=QC-ca)) ; Quebec
exten => s,n,ExecIf($["${AREA_MATCH}" = "431"]?Set(LOCATION_COUNTRY=MB-ca)) ; Manitoba
exten => s,n,ExecIf($["${AREA_MATCH}" = "437"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
exten => s,n,ExecIf($["${AREA_MATCH}" = "438"]?Set(LOCATION_COUNTRY=QC-ca)) ; Quebec
exten => s,n,ExecIf($["${AREA_MATCH}" = "450"]?Set(LOCATION_COUNTRY=QC-ca)) ; Quebec
exten => s,n,ExecIf($["${AREA_MATCH}" = "506"]?Set(LOCATION_COUNTRY=NB-ca)) ; New Brunswick
exten => s,n,ExecIf($["${AREA_MATCH}" = "514"]?Set(LOCATION_COUNTRY=QC-ca)) ; Quebec
exten => s,n,ExecIf($["${AREA_MATCH}" = "519"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
exten => s,n,ExecIf($["${AREA_MATCH}" = "548"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
exten => s,n,ExecIf($["${AREA_MATCH}" = "579"]?Set(LOCATION_COUNTRY=QC-ca)) ; Quebec
exten => s,n,ExecIf($["${AREA_MATCH}" = "581"]?Set(LOCATION_COUNTRY=QC-ca)) ; Quebec
exten => s,n,ExecIf($["${AREA_MATCH}" = "587"]?Set(LOCATION_COUNTRY=AB-ca)) ; Alberta
exten => s,n,ExecIf($["${AREA_MATCH}" = "604"]?Set(LOCATION_COUNTRY=BC-ca)) ; British Columbia
exten => s,n,ExecIf($["${AREA_MATCH}" = "613"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
exten => s,n,ExecIf($["${AREA_MATCH}" = "639"]?Set(LOCATION_COUNTRY=SK-ca)) ; Saskatchewan
exten => s,n,ExecIf($["${AREA_MATCH}" = "647"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario ; commented for testing
exten => s,n,ExecIf($["${AREA_MATCH}" = "705"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
exten => s,n,ExecIf($["${AREA_MATCH}" = "709"]?Set(LOCATION_COUNTRY=NL-ca)) ; Newfoundland and Labrador
exten => s,n,ExecIf($["${AREA_MATCH}" = "778"]?Set(LOCATION_COUNTRY=BC-ca)) ; British Columbia
exten => s,n,ExecIf($["${AREA_MATCH}" = "780"]?Set(LOCATION_COUNTRY=AB-ca)) ; Alberta
exten => s,n,ExecIf($["${AREA_MATCH}" = "782"]?Set(LOCATION_COUNTRY=PE-ca)) ; Prince Edward Island
exten => s,n,ExecIf($["${AREA_MATCH}" = "807"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
exten => s,n,ExecIf($["${AREA_MATCH}" = "819"]?Set(LOCATION_COUNTRY=QC-ca)) ; Quebec
exten => s,n,ExecIf($["${AREA_MATCH}" = "825"]?Set(LOCATION_COUNTRY=BC-ca)) ; British Columbia
exten => s,n,ExecIf($["${AREA_MATCH}" = "867"]?Set(LOCATION_COUNTRY=YT-ca)) ; Yukon, Northwest Territories, Nunavut
exten => s,n,ExecIf($["${AREA_MATCH}" = "873"]?Set(LOCATION_COUNTRY=QC-ca)) ; Quebec
exten => s,n,ExecIf($["${AREA_MATCH}" = "902"]?Set(LOCATION_COUNTRY=NS-ca)) ; Nova Scotia
exten => s,n,ExecIf($["${AREA_MATCH}" = "905"]?Set(LOCATION_COUNTRY=ON-ca)) ; Ontario
; US Illinois example configuration
; exten => s,n,ExecIf($["${AREA_MATCH}" = "217"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; exten => s,n,ExecIf($["${AREA_MATCH}" = "224"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; exten => s,n,ExecIf($["${AREA_MATCH}" = "309"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; exten => s,n,ExecIf($["${AREA_MATCH}" = "312"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; exten => s,n,ExecIf($["${AREA_MATCH}" = "331"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; exten => s,n,ExecIf($["${AREA_MATCH}" = "618"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; exten => s,n,ExecIf($["${AREA_MATCH}" = "630"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; exten => s,n,ExecIf($["${AREA_MATCH}" = "708"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; exten => s,n,ExecIf($["${AREA_MATCH}" = "773"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; exten => s,n,ExecIf($["${AREA_MATCH}" = "779"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; exten => s,n,ExecIf($["${AREA_MATCH}" = "815"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; exten => s,n,ExecIf($["${AREA_MATCH}" = "847"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; exten => s,n,ExecIf($["${AREA_MATCH}" = "872"]?Set(LOCATION_COUNTRY=IL-us)) ; Illinois
; Split LOCATION_COUNTRY into LOCATION and COUNTRY_CODE
; This makes it easier to work with in later logic
exten => s,n,Set(LOCATION=${CUT(LOCATION_COUNTRY,-,1)}) ; e.g., "ON" from "ON-ca"
exten => s,n,Set(COUNTRY_CODE=${CUT(LOCATION_COUNTRY,-,2)}) ; e.g., "ca" from "ON-ca"
; Check if *31 is pre-dialed
; This is a special code that triggers the use of alternative CIDs
exten => s,n,GotoIf($["${PREDIAL}" = "*31"]?star_31:location_match)
; *31 logic: Use alternative CIDs
exten => s,n(star_31),NoOp(*31 logic triggered)
exten => s,n,GotoIf($[${REGEX("^[0-9]+$" ${alternative_default_cid_${COUNTRY_CODE}})} = 1]?set_alternative_country_cid:check_us_cid) ; checks if an alternative_cid_<MATCHING COUNTRY> exists. If it does, call it. If it doesn't go to check_us_cid
exten => s,n(set_alternative_country_cid),Set(cid_to_use=${alternative_default_cid_${COUNTRY_CODE}})
exten => s,n,Goto(call_fun)
exten => s,n(check_us_cid),GotoIf($[${REGEX("^[0-9]+$" ${alternative_default_cid_us})} = 1]?set_alternative_us_cid:catchall_fallback_CID) ; checks if an alternative_cid_us exists. If it does, call it. If it doesn't go to [catchall_fallback_CID]
exten => s,n(set_alternative_us_cid),Set(cid_to_use=${alternative_default_cid_us})
exten => s,n,Goto(call_fun)
; Determine CID to use based on LOCATION and COUNTRY_CODE
; if a matching, valid LOCATION number exist, call it. If not go to country_code_logic
exten => s,n(location_match),NoOp(Entering LOCATION MATCHING)
exten => s,n,GotoIf($[${REGEX("^[0-9]+$" ${outbound_cid_${LOCATION}})} = 1]?location_logic:country_code_logic)
exten => s,n(location_logic),Set(cid_to_use=${outbound_cid_${LOCATION}})
exten => s,n,NoOp(LOCATION ${LOCATION} matches specific outbound_cid_${LOCATION})
exten => s,n,Goto(call_fun)
; if a matching, valid COUNTRY_CODE number exist, call it. If not go to check_default_cid
exten => s,n(country_code_logic),GotoIf($[${REGEX("^[0-9]+$" ${default_cid_${COUNTRY_CODE}})} = 1]?set_country_cid:check_default_cid)
exten => s,n(set_country_cid),Set(cid_to_use=${default_cid_${COUNTRY_CODE}})
exten => s,n,NoOp(COUNTRY_CODE ${COUNTRY_CODE} matches specific default_cid_${COUNTRY_CODE})
exten => s,n,Goto(call_fun)
; if default_cid is a valid number, call it. If not jump to [catchall_fallback_CID]
exten => s,n(check_default_cid),GotoIf($[${REGEX("^[0-9]+$" ${default_cid})} = 1]?set_default_cid:catchall_fallback_CID)
exten => s,n(set_default_cid),Set(cid_to_use=${default_cid})
exten => s,n,NoOp(default_cid ${default_cid} is a valid number. cid_to_use set accordingly)
exten => s,n,Goto(call_fun)
; Call Function
; This is where we actually set the CID and initiate the call
exten => s,n(call_fun),Set(CALLERID(num)=${cid_to_use})
exten => s,n,NoOp(Number to call is ${OUTNUM})
exten => s,n,NoOp(Outbound CID to use is ${cid_to_use})
exten => s,n,Return ; Go back to FreePBX's main dialplan
[catchall_fallback_CID]
; This is our last resort. If all other CID selections fail,
; we use a globally defined fallback CID.
exten => s,1,noop(Entering fallback configuration)
exten => s,n,NoOp(catchall_fallback_CID is ${catchall_fallback_CID})
; Check if we have a fallback CID. If not, apologize and hang up.
; This prevents showing a wrong or invalid CID.
exten => s,n,GotoIf($["${catchall_fallback_CID}" != ""]?set_caller_id)
exten => s,n,Playback(sorry-we-cannot-complete-your-call)
exten => s,n,Hangup()
; Set the fallback CID and return to the main dialplan
exten => s,n(set_caller_id),Set(CALLERID(num)=${catchall_fallback_CID})
exten => s,n,Return