Quick Start
From a bare server to a first draft invoice. Allow about an hour, most of it entering rates.
This page is the short version with a worked example throughout. Every step links to the page that explains it properly.
Every trunk starts non-billable. Nothing is charged until you classify your carrier trunks as billable in step 5.
A system that ingests CDRs perfectly and produces a $0.00 invoice has almost always skipped that step. If your first draft has recurring charges but no call charges, go straight to step 5.
The example we will use
Throughout this guide and the rest of the documentation, one worked example runs end to end:
| Partner | Northwind Telecom, billing in USD |
| PBX | one system, labelled PBX-01 |
| Customer | hq.northwind-example.com, 42 extensions, 18 DIDs |
| Carrier | one SIP trunk, carrier-primary |
| Plan | "Business Standard USD" |
By the end it produces an invoice of $887.29, and every figure on it is worked out in Worked Example.
1. Install
apt-get install -y wget unzip
cd /opt
wget https://cdn.vodia.net/vodia-billing/vodia-billing.zip
unzip vodia-billing.zip
cd vodia-billing
bash install.sh
You will be asked for a domain name, an admin email and an admin password. The domain must already resolve to this server; the installer checks and stops if it does not.
Then back up /opt/vodia-billing/.env off the server. It holds the key
that decrypts every PBX and accounting credential you are about to enter.
Open https://your-domain, sign in, and complete two-factor setup.
2. Add the PBX
Systems → Add system.
| Field | Example |
|---|---|
| Label | PBX-01 |
| PBX URL | pbx01.northwind-example.com |
| Port | 443 |
| Admin user | vodia-billing |
| Admin password | the dedicated account's password |
Test connection before going further. A green result means the credentials work.
3. Point the PBX at Vodia Billing
On the PBX, find the CDR URL setting and append this server. The field is space-delimited, so an existing receiver keeps working:
https://analytics.northwind-example.com/cdr https://billing.northwind-example.com/cdr
Make one test call, then check Spend → ingest counters.
You will probably see unknown_system climbing. That is expected: the CDRs
are arriving but the system ID is not set yet. Read the hash from the counter
and paste it into the system record's System ID field.
unknown_system: 3 (a3f9c2d18b7e4506)
^ paste this into Systems → System ID
Now accepted should climb instead.
4. Discover
Systems → Discover.
Discovery complete
Domains: 1 hq.northwind-example.com
Trunks: 2 carrier-primary, branch-link
Numbers: 18 hq.northwind-example.com
Check the number count. A domain showing Numbers: 0 will match no
number-based charges, and it is much better to see that now than on an
invoice.
5. Classify trunks — the step everybody forgets
Trunks. Both discovered trunks are non-billable. That is the default for everything, always.
| Trunk | PBX type | Proposal | Set to |
|---|---|---|---|
carrier-primary | register | billable | billable |
branch-link | gateway | transit | transit |
Click into each one and confirm. A proposal is shown but never applied for you.
The two mistakes cost differently. A trunk wrongly marked billable fabricates charge rows, the fraud detectors read them as real traffic, and once an invoice exists against them they cannot be un-rated.
A trunk wrongly marked transit leaves a gap you can find and re-rate.
So the safe default is an empty bill, not a wrong one — which is why this step cannot be skipped.
6. Create a rate plan
Plans → New plan, named Business Standard USD, currency USD, type
sell.
Then upload a rate sheet. A minimal working one, as CSV:
destination,prefix,charge_type,rate,connect_fee,increment_initial,increment_step,included,high_risk
US National,1,per_minute,0.012,0,60,60,,
US Toll Free,1800,free,0,0,0,0,true,
US Premium,1900,per_minute,2.50,0.75,60,60,,true
United Kingdom,44,per_minute,0.035,0,30,6,,
UK Mobile,4477,per_minute,0.14,0,30,6,,
Catch-all,*,per_minute,0.50,0,60,60,,true
The upload runs as a dry run first:
Dry run: Business Standard USD
Rows parsed: 6
Rows rejected: 0
Destinations: +6 added, 0 changed, 0 removed
Largest rate: 2.50/min (US Premium) — under ceiling of 10.00
Review, then commit.
Create a second plan the same way for cost, using your carrier's sheet. Cost is optional, but without it margin reporting is blank.
7. Price the recurring items
Plans → Recurring on the sell plan:
| Basis | Subtype / prefix | Frequency | Rate |
|---|---|---|---|
| entity | ext:user | monthly | 12.00 |
| entity | hunt | monthly | 5.00 |
| number | 1206 | monthly | 1.50 |
| flat | "Managed support plan" | monthly | 150.00 |
ext:* does not cover hunt groupsext:* matches only ext:-prefixed subtypes. Hunt groups, agent groups and
auto attendants are not extensions and need their own rows, or they will
report as unpriced and block finalization.
8. Build a tax profile
Tax → New profile, named US Telecom:
| Component | Kind | Rate | Applies to | Counts |
|---|---|---|---|---|
| Sales Tax | percent | 8.25% | usage, recurring, goods | |
| USF | percent | 1% | usage | |
| PUC | percent | 2% | usage | |
| 911 Fee | flat | 0.35 | recurring | entity |
Skip this step entirely for tenants you will connect to QuickBooks Online, which computes its own tax.
Note that applying a profile copies it onto the tenant. Editing the profile later does not change tenants that already have it.
→ Tax
9. Configure the tenant
Tenants → hq.northwind-example.com:
| Field | Example |
|---|---|
| Currency | USD |
| Sell plan | Business Standard USD |
| Cost plan | Carrier A Cost USD |
| Tax profile | US Telecom (apply) |
| Timezone | America/New_York |
| Billing recipients | accounts@customer-example.com |
| Invoice template | Default |
| Account reference | NW-1041 |
A domain you leave unconfigured is simply not billed.
10. Rate Test before you bill anything
Rate Test. Enter a number, the tenant and a duration.
Test +44 20 7946 0958, 1 minute 43 seconds:
Tenant: hq.northwind-example.com
Number: 442079460958
Sell: United Kingdom (prefix 44)
per_minute 0.035, connect 0.00, increments 30/6
103s billed as 108s = 1.80 min
= 0.06
Cost: UK Landline (prefix 44)
per_minute 0.009, increments 60/60
103s billed as 120s = 2.00 min
= 0.02
Margin: 0.04
Test at minimum: a local number, a national number, a mobile, an international, a premium number, and one you expect to be free. If any surprises you, fix the sheet now rather than after an invoice.
11. First bill run: drafts only
Bill Runs → New run, with finalize, email and export all unticked.
That prepares drafts and stops. Read them.
Bill run 2026-08 (prepare only)
hq.northwind-example.com draft 804.57 + 82.72 tax = 887.29
branch.northwind-example.com BLOCKED: no sell plan
Blocked tenants are reported and skipped. They never stop the rest of the run and never consume an invoice number.
12. Check the draft by hand
Once, properly, before you ever finalize:
- Do the line amounts sum to the subtotal?
- Compute each tax component yourself from the subtotals. Does each match?
- Is the extension count what the customer actually has?
- Is there a usage section at all? If not, see step 5.
Worked Example does this arithmetic in full for the invoice above.
13. Then go live
Only when drafts agree with reality:
- Enable finalize on the run or schedule.
- Connect Xero or QuickBooks Online and map customers.
- Set payment terms and reminders under Terms.
- Set fraud thresholds under Detection, leaving automatic response in report mode.
- Enable email.
Run both in parallel for one to two full billing cycles first. Keep the schedule at drafts-only for that whole period and reconcile per tenant.
See Running in parallel.
Commissioning checklist
Print this. The order matters, and each item is a thing that silently produces an empty or wrong invoice if skipped.
-
.envbacked up off the server - PBX added, Test connection green
- System ID set —
unknown_systemno longer climbing - PBX CDR URL appended,
acceptedclimbing - Discover run; domain, trunk and number counts all sane
- Every carrier trunk classified billable ← the one everybody forgets
- Internal trunks classified transit
- Sell plan created, rate sheet committed after dry run
- Cost plan created (or accepted that margin will be blank)
- Catch-all row present, and deliberately priced
- Recurring rates for every entity subtype in use, including hunt
groups and auto attendants separately from
ext:* - Number prefixes priced, and the tenant's DID list not empty
- Tax profile built and applied to the tenant
- Tenant currency, plan, timezone, recipients and template set
- Rate Test run on six representative numbers
- Archive CSV dry-run import reports
already_ratedfor a day already received by webhook - First bill run done drafts-only, and the draft checked by hand
- Fraud thresholds reviewed; automatic response left in report mode
When the invoice is empty or wrong
| Symptom | Almost always |
|---|---|
| No usage charges at all | Trunks still non-billable (step 5) |
| Recurring charges but no usage | Same thing |
Nothing anywhere, unknown_system climbing | System ID not set (step 3) |
Nothing, unknown_domain climbing | Tenant not configured (step 9) |
Nothing, unknown_trunk climbing | Discover not re-run after a trunk was added |
| Usage present but no tax | Tax profile built but never applied to the tenant |
| Extensions missing from recurring | ext:* used, and the missing ones are hunt groups |
| Will not finalize, "unpriced subtype" | A recurring rate is missing for that entity type |
| Will not finalize, "unrated legs" | A destination is missing from the sell plan |
| Amounts slightly off vs old system | Billing increments, then connect fees, in that order |