Reader setup
Before you start
- Admin access to VICIdial with permission to modify carriers and campaigns (modify_carriers and modify_campaigns) — Dial Prefix lives on the campaign screen, not a separate flag.
- Read-only database access through /etc/vicidial-readonly.cnf (see Create a read-only database account for safe VICIdial queries if you do not have one yet), for checking carrier records and later measuring real call outcomes.
- At least two carrier accounts already working on their own: one you intend to prefer for a specific destination range, and one to fall back to.
- Expected result
- You will have a documented destination-to-carrier mapping, a dialplan entry that routes that range to the cheaper carrier with an ordered fallback, and a read-only query that reports real connected minutes and answer rate per carrier instead of a rate-sheet guess.
- Before making changes
- Every pattern change here affects live call routing the moment it saves and regenerates. Test each new or changed carrier route on a narrow, already-verified destination before pointing full campaign volume at it, and keep a backup of vicidial_server_carriers before every change.
01 / 08
What least-cost routing actually means in VICIdial
Fast answer: least-cost routing (LCR), sometimes called Asterisk LCR when the routing logic lives in the dialplan rather than in a carrier's own product, is the practice of sending each outbound call over whichever working carrier is cheapest for that destination. VICIdial does not ship a rate-table engine that shops carriers automatically; it decides the carrier the same way Asterisk always has, through a campaign's dial prefix and a matching pattern inside a carrier's stored dialplan entry. Building least-cost routing here means designing those prefixes and patterns yourself, then verifying the result against real call data instead of a price list.
In plain language: a carrier is the telecom provider whose network actually carries your call to the public phone system; a trunk is the Session Initiation Protocol (SIP) or PJSIP connection VICIdial dials out through to reach that carrier; a DID, short for direct inward dialing, is a phone number a carrier assigns you so it can route inbound calls back to your system; a channel is one live call path Asterisk is currently handling; and a campaign is one calling project, meaning which leads get dialed, by which agents, under which dial settings.
ViciBox 12 ships agi-LCR-Route.agi in Asterisk's agi-bin directory. Its internal selection logic is not documented in any shipped reference doc, so treat it as a hook only: on the evidence available, it can select an outbound route using whatever logic you configure, and the carrier list, rates, and selection rules remain your responsibility to build and maintain. There is no shipped rate table that compares carriers for you. What every installation actually has, and what this guide builds on, is the dial prefix on a campaign or list and the dialplan entry stored against each row in vicidial_server_carriers; Asterisk's own pattern matching is the routing engine.
- Confirm which VICIdial revision and Asterisk version are installed before editing any carrier record.
- List every currently active row in vicidial_server_carriers before adding a new one.
- Write down the campaign's current dial_prefix value before changing anything.
Reference screenshots 3 images
Reference capture
Reference screens from the demo
Captured on an isolated VICIdial demo: Administration screens on September 24, 2026, and the idle Agent screen on August 11, 2026. Each caption states its own capture time, and every sanitized image helps you recognize a related screen; none proves that this article's call, command, or result occurred.Find carrier administration
Open to enlargeCapture details
This menu is a navigation map only; it does not show that any system-wide setting was changed or verified.Open Carrier Listings
Open to enlargeCapture details
Only stock example carrier rows are shown, with addresses replaced and registration values redacted; no real carrier is present. This does not prove registration, routing, audio quality, or a completed call.Check the allowed SIP-stack setting
Open to enlargeCapture details
This is a read-only view of system-wide settings with no credentials or addresses; it does not prove that a setting was changed or that an API request succeeded.02 / 08
How VICIdial actually decides which carrier a call uses
Every carrier your VICIdial install can dial through lives as one row in vicidial_server_carriers, with columns for carrier_id, protocol (SIP, PJSIP, PJSIP_WIZ, Zap, IAX2 or EXTERNAL), account_entry (the peer or endpoint configuration), and dialplan_entry, a block of raw Asterisk dialplan text you write yourself in Admin's carrier screen. When a carrier record changes, VICIdial's own configuration generator rewrites the server's generated Asterisk configuration and reloads it; you never hand-edit the generated include files, because the next regeneration overwrites them.
On the campaign side, vicidial_campaigns.dial_prefix (default 9) is the digit string VICIdial prepends to every number before it dials out. Admin's own Dial Prefix field explains the pattern it produces: enter 9 and a call to a North American number is placed as 91NXXNXXXXXX, or enter X for no prefix at all. A list can override that prefix for just its own leads, which is a second lever for steering one segment of numbers toward one route.
Put those two pieces together and the routing decision is just Asterisk pattern matching: the campaign builds a prefixed extension string and originates it into a context, falling back to the context named default when none is set; every active carrier's dialplan_entry fragment is concatenated into that context in carrier order; and Asterisk always picks whichever exten pattern is the most specific match for the dialed string. A pattern that pins a literal area code beats a general wildcard pattern for the same digits, and that specificity rule is the entire mechanism a working least-cost route depends on.
- Before you use it
- Historical exact-command capture from an authorized root shell. On your installation use the same approved root context or sudo mysql with the protected root0600 option file;do not loosen its permissions. Replace the carrier/campaign IDs privately. Historical exact-command/root-shell evidence is retained. For a current invocation outside that root shell, use the approved sudo mysql --defaults-file=/etc/vicidial-readonly.cnf client and confirm CURRENT_USER() and DATABASE();the changed invocation is not this capture.
mysql --defaults-extra-file=/etc/vicidial-readonly.cnf <<'SQL'SELECT carrier_id, protocol, active, carrier_description FROM vicidial_server_carriers WHERE carrier_id = '<CARRIER_ID>';SELECT campaign_id, dial_prefix FROM vicidial_campaigns WHERE campaign_id = '<CAMPAIGN_ID>';SQLCaptured demo response · 2026-09-24 22:25 UTC. Any filtering, hidden values or test-only data are identified in the capture label. Select Replay transcript to read the recorded output.
- Success looks like
- The carrier appears with active = Y, and the campaign's dial_prefix matches what you assumed it was.
- Stop if
- Stop if a carrier you thought was active shows N, or if the campaign's dial_prefix is not what your dialing scripts assume; fix that mismatch before adding a new route on top of it.
03 / 08
Step 1 — Map destination ranges to carriers before writing a pattern
Least-cost routing work starts on paper, not in Admin. Get a real quote or rate sheet from every carrier you are considering, then decide exactly which destination slice, a country code, an area code, or a defined number block, should move off your default carrier and onto the cheaper one. Only after that decision is written down should you translate it into an Asterisk extension pattern.
Asterisk patterns use N for any digit 2 through 9 and X for any digit 0 through 9, so the general North American pattern Admin documents, 91NXXNXXXXXX, matches any ten-digit number behind the dial prefix. Narrowing that pattern to one destination is just replacing wildcard positions with literal digits: 91212NXXXXXX matches only numbers in area code 212, while every other area code still falls to whatever pattern your default carrier already owns.
Write the new pattern down next to the carrier_id that should own it before you open Admin's carrier screen. If two active carriers could both match the same dialed string, Asterisk resolves the conflict by pattern specificity, not by carrier order in the table, so a vague pattern in a second carrier's dialplan_entry can silently steal traffic you meant to keep on the first one.
- Get a real quote or rate sheet from every candidate carrier before writing a single pattern.
- Decide the destination range, country code, area code, or number block, that will move to the cheaper carrier.
- Write the exact Asterisk pattern for that range on paper before touching Admin.
04 / 08
Step 2 — Write the cheap carrier's dialplan entry
Open the carrier record for the cheap route in Admin's add/modify carrier screen, the same form that names carrier_id, protocol, and the Dialplan Entry field, and paste in the pattern you designed in Step 1. The example below sends the area code 212 slice through a carrier named LOWCOST1 over PJSIP, stripping the one-digit dial prefix with the EXTEN:1 syntax before the number reaches the trunk. The first line calls VICIdial's own call-logging AGI (agi://127.0.0.1:4577/call_log) before anything else happens — every Dialplan Entry in this library keeps that line first, because replacing it with a NoOp or any other action silently stops that call from being logged at all.
Save the record, then let VICIdial's own configuration generator rebuild and reload the dialplan. Do not edit the generated extensions file directly, because the next save or keepalive cycle overwrites hand edits without warning.
- Before you use it
- Paste this into the Dialplan Entry field for carrier_id LOWCOST1 in Admin's carrier screen; do not save it to a file on the server — VICIdial's own generator writes the real config.
exten => _91212NXXXXXX,1,AGI(agi://127.0.0.1:4577/call_log) same => n,Dial(PJSIP/${EXTEN:1}@LOWCOST1,${CAMPDTO},To) same => n,Hangup()This sample is a template or reading aid, not a terminal command. There is no output to show.
- Success looks like
- After saving and the normal regeneration cycle, dialplan show for the pattern reports it under the expected context, with LOWCOST1 as the Dial target and no other carrier's pattern reported instead.
- Stop if
- Stop if the generated dialplan shows a different carrier handling the pattern, if the endpoint name in Dial() does not match an active carrier_id built the way Add and test a VICIdial SIP carrier shows, or if the pattern is broader than the one range you decided on in Step 1.
05 / 08
Step 3 — Order the attempts so a failure falls through to the next carrier
A single-attempt route is a bet: if LOWCOST1 is down, congested, or simply fails to answer, that failure just ends the call with nothing to show for it. Add a second attempt in the same carrier's dialplan_entry that checks the result of the first Dial() and falls through to another carrier's endpoint before giving up — but only for the failures that are actually the carrier's fault.
This example considers fallback on CHANUNAVAIL or CONGESTION. These statuses do not by themselves prove where failure occurred; test the provider-specific behavior and duplicate-call risk before enabling fallback. BUSY, NOANSWER, and ANSWER all mean the customer's line was reached in some way; retrying those on a second carrier just double-dials the same person, which is worse than the original failure.
The order of the two Dial() lines is the entire failover policy: whichever carrier's line runs first gets first refusal at every call matching that pattern, and every later Dial() only fires once every earlier one has already failed on one of those two carrier-side statuses. Keep that order deliberate and documented, because it is easy to reverse by accident during a later edit.
- Before you use it
- Replace the single-attempt block from Step 2 with this version, pasted the same way into carrier_id LOWCOST1's Dialplan Entry field, once you have confirmed BACKUP1 is an active, working carrier on its own.
exten => _91212NXXXXXX,1,AGI(agi://127.0.0.1:4577/call_log) same => n,Dial(PJSIP/${EXTEN:1}@LOWCOST1,${CAMPDTO},To) same => n,GotoIf($["${DIALSTATUS}" = "CHANUNAVAIL" | "${DIALSTATUS}" = "CONGESTION"]?backup) same => n(done),Hangup() same => n(backup),NoOp(LOWCOST1 failed with status ${DIALSTATUS}, trying BACKUP1) same => n,Dial(PJSIP/${EXTEN:1}@BACKUP1,${CAMPDTO},To) same => n,Hangup()This sample is a template or reading aid, not a terminal command. There is no output to show.
- Success looks like
- A forced CHANUNAVAIL or CONGESTION on LOWCOST1 still completes the call over BACKUP1, and DIALSTATUS reflects the second attempt's real outcome; a BUSY or NOANSWER on LOWCOST1 ends the call there, exactly as it would with no fallback at all.
- Stop if
- Stop if the call also fails over BACKUP1 in a case where BACKUP1 is known to work alone; that points at a dialplan ordering mistake rather than a carrier problem.
06 / 08
Why cheapest is not always right: ASR, audio, and caller ID
A cheaper per-minute rate only pays off if the call actually connects and stays connected. The answer-seizure ratio (ASR), the share of dialed attempts that get answered, is the first number to ask a low-cost carrier for, because a carrier that is a fraction of a cent cheaper but answers ten points fewer of your attempts is not saving you anything: your dialer just redials the same lead more times to get one live conversation, burning agent time and hopper capacity along the way.
Audio quality is the second trade-off. A route with choppy audio, one-way audio, or excess post-dial delay produces hang-ups and repeat calls that a rate sheet never shows, and those repeat attempts count against the same lead's call limits and local call time window.
Caller-ID and identity treatment is the third. Some low-cost routes rewrite, strip, or fail to pass your caller ID at all, and a carrier that changes what the called party sees can affect both answer behavior and how that call is perceived downstream. None of that shows up on a per-minute quote either.
- Ask each carrier for its answer-seizure ratio and typical post-dial delay, not just its rate.
- Listen to a handful of live calls on the cheap route before trusting it with volume.
- Check whether the carrier passes your caller ID unchanged or substitutes its own.
07 / 08
Measure duration and reconcile the provider’s actual charge
Keep four quantities separate: attempted calls, carrier-answered seconds, agent talk time and billed duration. An ANSWER signal does not establish a human conversation; vicidial_log.length_in_sec must be checked against its installed report meaning before being called connected seconds.
The retained query is an unexecuted diagnostic aggregate, not a bill. Validate one known permitted call’s uniqueid/channel mapping and one-to-one join first;include unmatched attempts separately so a missing call row cannot inflate answer rate. If that mapping is unresolved, stop the SQL comparison and reconcile provider records by their supported identifiers.
For actual cost, export the authorized provider CDR/invoice and its rate plan for the same closed interval. Record per-attempt/setup charges, billing increment, minimum, monthly allocation and taxes separately. Use the invoice’s billed units rather than multiplying an unverified VICIdial duration.
- Choose one closed interval and validate the carrier/call identity mapping against a known authorized test. Record unmatched and duplicate rows.
- Reproduce the provider invoice’s billed units and usage charge using its exact rounding/minimum/fee contract. Compare line totals with the invoice and resolve discrepancies.
- Divide reconciled spend by the explicitly chosen denominator: billed minute, carrier-answered minute or separately verified successful conversation. Label it exactly;do not relabel answer time as human talk.
- Compare the same workload and quality criteria, including failures, post-dial delay and caller-ID treatment. Reject a cheaper apparent total if mapping or invoice reconciliation is unresolved.
- Before you use it
- Illustration only: the channel extraction and inner join are not verified for your system. Do not use this query for billing until the mapping, join cardinality, missing rows and duration semantics are independently verified. Open the approved client with sudo mysql --defaults-file=/etc/vicidial-readonly.cnf and confirm SELECT CURRENT_USER(), DATABASE() before entering this scoped SQL. Do not widen the credential-file permissions.
SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(vcl.channel, '-', 1), '/', -1) AS carrier_id, COUNT(*) AS attempts, SUM(vcl.dialstatus = 'ANSWER') AS answered, ROUND(SUM(vcl.dialstatus = 'ANSWER') / COUNT(*), 4) AS answer_seizure_ratio, SUM(CASE WHEN vcl.dialstatus = 'ANSWER' THEN vl.length_in_sec ELSE 0 END) AS connected_seconds, ROUND(SUM(CASE WHEN vcl.dialstatus = 'ANSWER' THEN vl.length_in_sec ELSE 0 END) / 60, 2) AS connected_minutesFROM vicidial_carrier_log vclJOIN vicidial_log vl ON vl.uniqueid = vcl.uniqueidWHERE vcl.call_date >= CURDATE() - INTERVAL 1 DAY AND vcl.channel LIKE '%<CARRIER_ID>%'GROUP BY carrier_idORDER BY connected_minutes DESC;Captured demo response · 2026-09-24 22:25 UTC. Any filtering, hidden values or test-only data are identified in the capture label. Select Replay transcript to read the recorded output.
- Success looks like
- Validate the carrier mapping and join against known test calls and invoice records; plausible totals alone do not prove accuracy.
- Stop if
- Zero rows on a carrier you know took calls in that window means the channel-naming assumption does not hold for it; a carrier using a technology or dial string this pattern does not match will need its own extraction logic before the numbers can be trusted.
You should seeThe example shows why a lower rate can cost more after minimums/fees. Your result needs provider records;the retained query alone cannot establish conversation minutes or actual charge.
08 / 08
Troubleshoot a route change and prepare a rollback
Back up vicidial_server_carriers before you touch it: mariadb-dump does not read the read-only option file's [mysql] database the way mysql -e does, so give it the database name explicitly — find it in /etc/vicidial-readonly.cnf's own [mysql] section as <VARDB_database>, then run mariadb-dump --single-transaction --skip-comments --result-file=/root/vicidial-carrier-backups/vicidial_server_carriers.sql <VARDB_database> vicidial_server_carriers. This writes a sensitive backup file. Confirm a protected destination, required privileges and the approved backup procedure first; test restoration separately.
After saving a new or changed carrier record and letting the normal regeneration cycle run, verify the live dialplan and endpoints directly on the Asterisk CLI rather than assuming the save worked. A pattern that looks right in Admin can still fail to load if the dialplan_entry text has a syntax mistake the generator silently drops. asterisk -rx 'dialplan show <DID_PATTERN>@default' is the most direct check, but run it only against your own install: on a box that carries other carriers' real traffic, that same command will show whichever carrier's pattern actually owns that digit string, not necessarily the one you just wrote.
If a rollback is needed, reinstate the previous verified dialplan_entry text through Admin, save again, and let the regeneration cycle rewrite the dialplan a second time; then repeat the same verification checks before sending live traffic back through that carrier. VICIdial's own save-triggered regeneration handles reloading Asterisk for you — do not declare the rollback finished until you have re-verified the pattern, not just re-run the save.
- Before you use it
- Run these immediately after every save, whether it is the first change or a rollback, replacing <CARRIER_ID> and <BACKUP_CARRIER_ID> with your real carrier_id values.
asterisk -rx 'pjsip show endpoint <CARRIER_ID>'asterisk -rx 'pjsip show endpoint <BACKUP_CARRIER_ID>'Captured demo response · 2026-09-24 22:25 UTC. Any filtering, hidden values or test-only data are identified in the capture label. Select Replay transcript to read the recorded output.
- Success looks like
- pjsip show endpoint reports each referenced carrier as available, matching the carrier you intended for this route.
- Stop if
- 'Unable to find object' for your backup carrier is expected until you have actually built and activated it — VICIdial's generator only writes active carriers into the live config. Stop and roll back immediately if an endpoint you believe is already active instead reports Unavail; do not leave live traffic pointed at an unverified pattern while you investigate.
Checks and sources
What was checked
- vicidial_server_carriers (protocol, account_entry, dialplan_entry) and vicidial_campaigns.dial_prefix are the stored columns that decide outbound routing; ViciBox 12 also ships an agi-LCR-Route.agi routing hook in Asterisk's agi-bin directory, but its internal selection logic is not documented in any shipped reference doc, so carrier, rate, and configuration responsibility is treated here as external to VICIdial, not something this guide can benchmark against a shipped rate table.
- The historical carrier-log/table inspection supports the field names only. No production call-to-carrier mapping, join cardinality, conversation duration, rate or invoice reconciliation was verified. The new invoice table is explicitly synthetic arithmetic.
Primary references
Sources
- VICIdial PJSIP SupportVICIdial · accessed August 5, 2026
- VICIdial StatusesVICIdial · accessed August 5, 2026
- Configuring res_pjsip (Asterisk PJSIP channel driver)Asterisk · accessed August 5, 2026
- Official VICIdial SVN and configuration guidanceVICIdial · accessed August 5, 2026
- Dial() dialplan application (DIALSTATUS values)Asterisk · accessed September 23, 2026


