A2Billing VoIP Billing Engine: Architecture, AGI Integration, and Rate Management
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:
| Table | Purpose |
|---|---|
| cc_card | Customer accounts, PINs, and balances |
| cc_call | CDR — every call detail record with cost and duration |
| cc_did | DID number inventory and routing rules |
| cc_did_use | DID assignment to customer accounts |
| cc_tariffplan | Rate deck definitions and tier names |
| cc_rate | Per-destination rates within each tariff plan |
| cc_invoice | Generated postpaid invoices |
| cc_agent | Agent and reseller sub-accounts with markup settings |
| cc_voucher | Top-up voucher codes for prepaid top-up |