VICIfast
Guides & tutorials

Least-cost routing of outbound calls with agi-LCR-Route.agi

Learn how agi-LCR-Route.agi looks up the cheapest carrier for each outbound call by NPANXX prefix and sets the LCRTRUNK dialplan variable before dialing.

VICIfast Support
··4 min read
Least-cost routing of outbound calls with agi-LCR-Route.agi

When you have contracts with multiple carriers and the per-minute rate differs by destination, you want each outbound call to travel the cheapest route available. That is what least-cost routing (LCR) does: before a call is placed, it checks a rate table and picks the lowest-cost Carrier string for the number being dialed. VICIdial implements this through agi-LCR-Route.agi, an AGI script that runs inside the Asterisk Dialplan and sets a variable the Dial() command reads.

The lookup key is the NPANXX: the first six digits of a North American phone number, consisting of the three-digit area code (NPA) and the three-digit exchange (NXX). This six-digit prefix identifies the rate center that determines the termination cost on most US and Canadian calling rate sheets.

You reach for LCR when carrier economics, not technical routing, drive the decision. If you only have one Server trunk there is nothing to choose between, and the simpler approach is to point your dial pattern straight at it. LCR earns its keep once you carry meaningful outbound volume across two or more carriers whose rates vary by region — for example one provider that is cheap for the West Coast and another that is cheaper for the Northeast. Instead of building a separate dial prefix and pattern per carrier and asking agents to remember which to use, you load every carrier's rate sheet into one table and let the script pick the cheapest path on a per-call basis. Over a month of dialing, the difference between a $0.0064 and a $0.0076 CID (caller ID)-destination rate adds up.

How the routing decision works

flowchart TD
  A[Outbound call triggered] --> B[call_log.agi runs]
  B --> C[agi-LCR-Route.agi runs with NPANXX]
  C --> D{NPANXX found in lcr table?}
  D -- Yes --> E[Set LCRTRUNK to cheapest carrier string]
  D -- No --> F[Keep LCRTRUNK at default value from globals]
  E --> G[Dial via LCRTRUNK]
  F --> G
  G --> H[Call connects through chosen carrier]

The script queries the lcr table for the NPANXX passed as its argument. If a match exists, it picks the carrier string with the lowest rate and stores it in the LCRTRUNK channel variable. If no match is found, LCRTRUNK keeps whatever value was set before the script ran — so you must define a default in the dialplan's [globals] section.

Dialplan setup

The pattern for outbound VICIdial calls in the [default] context looks like this:

[globals]
LCRTRUNK=SIP/provider1    ; default LCR trunk

[default]
exten => _91NXXNXXXXXX,1,AGI(agi://127.0.0.1:4577/call_log)
exten => _91NXXNXXXXXX,n,AGI(agi-LCR-Route.agi,${EXTEN:2:6})
exten => _91NXXNXXXXXX,n,Dial(${LCRTRUNK}/${EXTEN:2},,To)
exten => _91NXXNXXXXXX,n,Hangup

The expression ${EXTEN:2:6} strips the leading dial prefix digits and takes the next six characters, which gives the NPANXX. The result of the LCR lookup is then used by Dial(${LCRTRUNK}/${EXTEN:2},,To) to place the call through the selected Trunk.

The lcr table structure

The lcr table holds three columns: npanxx (a six-digit integer), rate (a decimal rate string), and carrier_string (up to 20 characters, matching what you would put before the slash in a SIP Dial command). A unique index covers all three columns. For a given NPANXX with two carriers, the table might hold two rows: one with rate 0.0064 for SIP/provider1 and one with rate 0.0076 for SIP/provider2. The script picks the lower rate row.

The SQL to create this table:

CREATE TABLE IF NOT EXISTS lcr (
  npanxx mediumint NOT NULL,
  rate varchar(10) NOT NULL,
  carrier_string varchar(20) NOT NULL
);
ALTER TABLE lcr ADD UNIQUE INDEX (npanxx, rate, carrier_string);

A quick check of one prefix shows how the data looks in practice. A query like SELECT npanxx, rate, carrier_string FROM lcr WHERE npanxx='734223' might return two rows for the same NPANXX — one carrier at 0.0064 and another at 0.0076 — and the script will pick the 0.0064 row because it is the cheaper rate. The unique index across all three columns means a given carrier can appear only once per NPANXX at a given rate, which keeps duplicate rows out of the table when you reload a rate sheet.

Rate sheets from carriers are not in a standard format, so there is no built-in import tool. You need to write a custom loader script to populate the lcr table from whatever format your carriers provide. An example loader is included in the extras directory of the astguiclient source code under lcr_loader_example.pl.

Once the table is populated, LCR works on every outbound Campaign automatically, selecting the best SIP trunk for each destination without manual dial prefix management. For context on how this fits alongside other VICIdial routing tools and the HTTP API layer, see the VICIdial API and AGI overview. If you are also exploring how to pass source parameters through API calls, the vicidial-api-source-parameter guide covers that usage.

If you want a managed VICIdial server where the dialplan is already in place and you can start populating the lcr table immediately, every VICIfast plan ships a fully configured Asterisk instance ready for your carrier credentials.

About VICIfast LLC

VICIfast LLC operates a managed VICIdial hosting + BYOI service for outbound and inbound call centers. We run the dialers, the carriers, the recordings pipeline, and the compliance plumbing so operators don’t have to.

Citing this article

VICIfast Engineering. “Least-cost routing of outbound calls with agi-LCR-Route.agi”. VICIfast LLC, June 28, 2026. Retrieved from https://vicifast.com/blog/use-agi-lcr-route

Have questions?

You might be interested in

VICIfast newsletter

Liked this? Get the next one in your inbox.

We ship the kind of stuff you just read — concrete, numbers-first, no drip. One email when a new post goes live. Unsubscribe in one click.

Comments

Comments are reviewed before they appear. We never publish your email.

No comments yet — be the first.