Konstas.Engineering Notes
Networking · Cybersecurity · Infrastructure
Telecom

A2Billing VoIP Billing Engine: Architecture, AGI Integration, and Rate Management

Articles · 2026 · Ioannis (John) Konstas

Asterisk handles the media and signaling for a call — it has no concept of what that call should cost, or whether the caller can afford it. Left alone, a PBX will happily connect a prepaid customer with a zero balance to an international destination and let the call run until someone notices the invoice. A2Billing closes that gap: an open-source (AGPLv3) billing engine that hooks into the dialplan itself, so authorization happens before the call connects, not after it's already cost money.

The mechanism is the Asterisk Gateway Interface (AGI) — Asterisk calls out to an external script mid-dialplan and waits for an answer before proceeding. A2Billing uses that hook to check the account balance, look up the destination rate, calculate the maximum call duration the balance actually covers, and reject the call outright if there's nothing left to charge against.

Architecture

Three pieces do the work: the AGI script sits between Asterisk and the database, and a web interface sits on top for administration and customer self-service.

┌──────────────────────────────────────────┐
│              ASTERISK                    │
│  extensions.conf calls AGI script        │
└──────────────▬──────────────────────────┘
               │ AGI call (real-time auth)
┌──────────────▼──────────────────────────┐
│           A2BILLING AGI                  │
│  a2billing.php                           │
│  - Check customer balance                │
│  - Authorise call                        │
│  - Calculate max call duration           │
│  - Deduct credit in real time            │
└──────────────▬──────────────────────────┘
               │ Reads/Writes
┌──────────────▼──────────────────────────┐
│              MYSQL DATABASE              │
│  Customers, accounts, rates, CDRs        │
└──────────────▬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│         A2BILLING WEB INTERFACE          │
│  Admin │ Agent │ Customer self-service  │
└──────────────────────────────────────────┘

On top of core prepaid/postpaid billing, the same platform handles the things that tend to get bolted on separately: calling-card access via PIN, DID inventory and per-account assignment, reseller sub-accounts with their own markup over cost, and a self-service portal so customers can top up and review call history without a support ticket. USD and international currencies both work out of the box.

The AGI hook, in the dialplan

The whole system hinges on one line placed before the actual Dial():

; extensions.conf — A2Billing called before every outbound call
[from-pstn-inbound]
exten => _NXXNXXXXXX,1,AGI(a2billing.php,1)
exten => _NXXNXXXXXX,n,Dial(PJSIP/${EXTEN},30)
exten => _NXXNXXXXXX,n,Hangup()

[outbound-calls]
exten => _1NXXNXXXXXX,1,AGI(a2billing.php,1)
exten => _1NXXNXXXXXX,n,Dial(PJSIP/${EXTEN}@carrier-trunk,60)
exten => _1NXXNXXXXXX,n,Hangup()

When Asterisk hits AGI(a2billing.php,1), execution pauses on that channel while the script looks up the account, checks the balance, resolves the rate for the destination prefix, and calculates the maximum duration that balance actually covers. Zero balance or an unrated destination means the call gets rejected right there — the dialplan never reaches Dial() at all.

Rate decks, and why the match order matters

A rate deck is just a per-destination price list, but the matching logic is the part worth understanding before populating one:

-- Example rate entries (cc_rate table)
destination         | prefix | rate_usd_per_min
--------------------|--------|------------------
USA Domestic        | 1      | 0.0120
Mexico Mobile       | 5219   | 0.1800
Dominican Republic  | 1809   | 0.0850
Brazil Mobile       | 5511   | 0.2200
UK Landline         | 4411   | 0.0250

Matching is always longest-prefix-first — a call to a Brazil mobile number (5511...) matches the specific 5511 entry over the generic 55 Brazil entry, the same logic as IP routing. Get that backwards in a manually-edited rate table and every mobile call in Brazil silently bills at the landline rate. Separate rate decks can exist per customer tier — retail, wholesale, reseller — each pointed at the same prefix logic with different numbers.

The calling-card path

Prepaid access without an account at all — PIN in, destination in, call connects at whatever rate the card's own deck specifies:

Customer dials access number (DID)
         │
         ▼
Asterisk IVR answers
"Please enter your PIN followed by #"
         │
         ▼
A2Billing AGI validates PIN → checks balance
         │
         ▼
"Please enter the number you wish to call"
         │
         ▼
A2Billing calculates max duration at destination rate
         │
         ▼
Call connects → real-time credit deduction per second
         │
         ▼
"You have 2 minutes remaining" (warning prompt)
         │
         ▼
Credit exhausted → call terminated → CDR written

What's actually in the database

Nine tables cover the whole lifecycle — accounts and balances, the call detail records everything else derives from, DID inventory, rate structure, and the reseller layer on top:

TablePurpose
cc_cardCustomer accounts, PINs, and balances
cc_callCDR — every call detail record with cost and duration
cc_didDID number inventory and routing rules
cc_did_useDID assignment to customer accounts
cc_tariffplanRate deck definitions and tier names
cc_ratePer-destination rates within each tariff plan
cc_invoiceGenerated postpaid invoices
cc_agentAgent and reseller sub-accounts with markup settings
cc_voucherTop-up voucher codes for prepaid top-up

← All articles

Research and direction by Ioannis (John) Konstas; compiled and finalized with AI assistance.

👁· views