Wholesale Softswitch: Route Simulator, Multi-Match Routes, and Billing That Adds Up
Wholesale voice is a business run on spreadsheets and margins, so the platform running it should speak spreadsheet: exact route matches, predictable rounding, and a way to answer “why did this call route the way it did” without guesswork. The latest RustPBX wholesale addon update focuses on exactly that.
Route Items That Match the Real World

Real route entries rarely match one pattern. A single destination code usually has mobile and fixed ranges, and the same customer often sends several CLI pools. Route items now support multiple caller and callee patterns per item, so one item covers what used to take five — with a stable id column and deterministic ordering so the same input always evaluates the same way.
Matching also got more truthful:
- Specific reject reasons per trunk — when a trunk declines a call, the CDR and the API say why (out of credit, circuit open, no matching route) instead of a generic 403/486.
- The wholesale RouteInvite node chains with the default router as a fallback, and wholesale routes are exposed to the console routing stack — you can see, in one place, how wholesale and core routing interact for a given call.
- A route simulator in the console: paste a caller/callee pair, and it shows which route items matched and which trunk would win. No more test calls to verify a rate deck change.
Billing Precision

Three changes that eliminate the “numbers don’t add up” conversation:
- Billing rounds up to the nearest second consistently, using the billing date settings that are now validated at save time (bad date configs were a real source of silent mis-billing).
- Negative charging amounts are allowed — credits and post-call adjustments post through the same ledger as charges, so a dispute correction is one entry instead of an external spreadsheet.
- CDRs carry the matched wholesale route ids in their billing context, tying every rated call back to the exact rate-deck entry that priced it.
Call-record ingestion also moved to batched hooks, which matters at volume: a busy wholesale box writing thousands of CDRs no longer taxes the call path one row at a time.
Operational Polish

insufficient_fundsrejections downgraded to WARN severity — a customer running out of credit is a business event, not a platform fault, and it no longer pages the on-call engineer.- Mutation APIs enforce write permissions consistently across tenants, rate decks, and routes.
- The console UI got the cc-style treatment: subnav tabs, searchable/paginated lists, a dashboard sparkline, and the route simulator.
- MySQL compatibility fix for versions before 8.0.17 (window-function-free query plans), and addon migrations now recover from stale migration records at startup.
Upgrading
The wholesale addon ships inside the addon-wholesale feature of RustPBX 0.5. Existing rate decks and routes migrate automatically; see the wholesale docs for the full walkthrough, or jump straight to the routing profiles guide for the new multi-pattern route items.
Running It in a Cluster
Wholesale is where clustering stops being academic — this is the workload that outgrows one box first:
- Route simulation works cluster-wide — the diagnostics endpoint asks every peer, so you see the same decision regardless of which node handles the call.
- CDR ingestion is batched — high-CPS termination writes thousands of records per minute without touching the media path.
- Circuit breakers are per-profile and sliding-window — one bad carrier route opens without taking down the whole carrier.
- Session ownership is explicit — an operator on the console can inspect and control a call owned by any node.
The wholesale cluster docs cover the topology; the short version is: shared MySQL/PostgreSQL, identical config via GitOps, and let the session registry do the routing.