Skip to main content

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.

The one thing everybody forgets

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:

PartnerNorthwind Telecom, billing in USD
PBXone system, labelled PBX-01
Customerhq.northwind-example.com, 42 extensions, 18 DIDs
Carrierone 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.

Installation

2. Add the PBX

Systems → Add system.

FieldExample
LabelPBX-01
PBX URLpbx01.northwind-example.com
Port443
Admin uservodia-billing
Admin passwordthe 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.

TrunkPBX typeProposalSet to
carrier-primaryregisterbillablebillable
branch-linkgatewaytransittransit

Click into each one and confirm. A proposal is shown but never applied for you.

Why the default is non-billable and why proposals are not auto-applied

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.

Systems, Tenants and Trunks

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.

Rate Plans and Sheets

7. Price the recurring items

Plans → Recurring on the sell plan:

BasisSubtype / prefixFrequencyRate
entityext:usermonthly12.00
entityhuntmonthly5.00
number1206monthly1.50
flat"Managed support plan"monthly150.00
ext:* does not cover hunt groups

ext:* 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.

Recurring Charges

8. Build a tax profile

Tax → New profile, named US Telecom:

ComponentKindRateApplies toCounts
Sales Taxpercent8.25%usage, recurring, goods
USFpercent1%usage
PUCpercent2%usage
911 Feeflat0.35recurringentity

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:

FieldExample
CurrencyUSD
Sell planBusiness Standard USD
Cost planCarrier A Cost USD
Tax profileUS Telecom (apply)
TimezoneAmerica/New_York
Billing recipientsaccounts@customer-example.com
Invoice templateDefault
Account referenceNW-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.

Rate Test

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.

Bill Runs and Schedules

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:

  1. Enable finalize on the run or schedule.
  2. Connect Xero or QuickBooks Online and map customers.
  3. Set payment terms and reminders under Terms.
  4. Set fraud thresholds under Detection, leaving automatic response in report mode.
  5. Enable email.
If you are replacing an existing billing system

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.

  • .env backed up off the server
  • PBX added, Test connection green
  • System ID set — unknown_system no longer climbing
  • PBX CDR URL appended, accepted climbing
  • 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_rated for 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

SymptomAlmost always
No usage charges at allTrunks still non-billable (step 5)
Recurring charges but no usageSame thing
Nothing anywhere, unknown_system climbingSystem ID not set (step 3)
Nothing, unknown_domain climbingTenant not configured (step 9)
Nothing, unknown_trunk climbingDiscover not re-run after a trunk was added
Usage present but no taxTax profile built but never applied to the tenant
Extensions missing from recurringext:* 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 systemBilling increments, then connect fees, in that order

Troubleshooting