Wholesale Complete Example
This section walks through a complete example of building a Wholesale voice transit platform from scratch.
Scenario Description
Company: A voice carrier providing domestic/international voice termination to 3 downstream customers (tenants).
Upstream Carriers:
- Carrier A: Domestic routes, SIP trunk
10.0.1.100:5060 - Carrier B: International routes, SIP trunk
10.0.2.50:5060
Downstream Customers:
- Customer X: 50K calls/day, primarily domestic
- Customer Y: 20K calls/day, domestic + international
- Customer Z: 10K calls/day, international only
Step 1: Database Preparation
# MySQL
mysql -u root -p <<EOF
CREATE DATABASE rustpbx CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'rustpbx'@'%' IDENTIFIED BY 'StrongP@ssw0rd';
GRANT ALL PRIVILEGES ON rustpbx.* TO 'rustpbx'@'%';
FLUSH PRIVILEGES;
EOF
Step 2: config.toml
http_addr = "0.0.0.0:8080"
database_url = "mysql://rustpbx:StrongP@ssw0rd@10.0.0.100:3306/rustpbx"
external_ip = "203.0.113.10"
[proxy]
addr = "0.0.0.0"
udp_port = 5060
modules = ["acl", "auth", "registrar", "call"]
media_proxy = "auto"
addons = ["wholesale"]
[proxy.wholesale]
route_cache_capacity = 10000
route_cache_ttl_secs = 30
[proxy.wholesale.circuit_breaker]
failure_threshold = 5
open_duration_secs = 30
half_open_probes = 1
failure_codes = [503, 408, 504]
[console]
session_secret = "my-console-secret-2026"
Step 3: Configure SIP Trunks
mkdir -p config/trunks
config/trunks/carrier_a.toml (domestic carrier):
[[trunk]]
name = "carrier-a"
dest = "sip:10.0.1.100:5060"
backup_dest = "sip:10.0.1.101:5060"
direction = "outbound"
codec = ["pcmu", "pcma", "g729"]
max_calls = 300
max_cps = 50
weight = 100
[trunk.auth]
username = "myacct_a"
password = "pass_a"
config/trunks/carrier_b.toml (international carrier):
[[trunk]]
name = "carrier-b"
dest = "sip:10.0.2.50:5060"
direction = "outbound"
codec = ["pcmu", "pcma", "g729", "g722"]
max_calls = 200
max_cps = 30
weight = 100
[trunk.auth]
username = "myacct_b"
password = "pass_b"
Step 4: Start the Service
docker run -d --name rustpbx \
-v $(pwd)/config.toml:/app/config.toml \
-v $(pwd)/config:/app/config \
-p 8080:8080 -p 5060:5060/udp \
docker.cnb.cool/miuda.ai/rustpbx:latest
Step 5: Web Console Configuration
Access http://203.0.113.10:8080, create a superuser, and enter the console.
5.1 Create Rate Decks
Wholesale → Rate Decks → Create New:
| Name | Type |
|---|---|
| buy-carrier-a | Buy |
| buy-carrier-b | Buy |
| sell-domestic | Sell |
| sell-international | Sell |
Add rate entries:
buy-carrier-a (buy-domestic):
| Prefix | Rate | Min Duration | Increment |
|---|---|---|---|
| 8610 | 0.025 | 6 | 6 |
| 8621 | 0.025 | 6 | 6 |
| 86 | 0.035 | 6 | 6 |
buy-carrier-b (buy-international):
| Prefix | Rate | Min Duration | Increment |
|---|---|---|---|
| 1 | 0.06 | 6 | 6 |
| 44 | 0.05 | 6 | 6 |
| 81 | 0.07 | 6 | 6 |
sell-domestic (sell-domestic):
| Prefix | Rate | Min Duration | Increment |
|---|---|---|---|
| 8610 | 0.04 | 6 | 6 |
| 8621 | 0.04 | 6 | 6 |
| 86 | 0.05 | 6 | 6 |
sell-international (sell-international):
| Prefix | Rate | Min Duration | Increment |
|---|---|---|---|
| 1 | 0.10 | 6 | 6 |
| 44 | 0.08 | 6 | 6 |
| 81 | 0.12 | 6 | 6 |
5.2 Configure Trunk Wholesale Parameters
Wholesale → Trunk Management:
- carrier-a → Buy rate deck:
buy-carrier-a, enable circuit breaker - carrier-b → Buy rate deck:
buy-carrier-b, enable circuit breaker
5.3 Create Routing Profiles
Wholesale → Routing Profiles → Create New:
Profile “profile-domestic” (domestic routing):
| Priority | Trunk | Prefix | Weight |
|---|---|---|---|
| 10 | carrier-a | 86 | 100 |
Profile “profile-full” (domestic + international):
| Priority | Trunk | Prefix | Weight |
|---|---|---|---|
| 10 | carrier-a | 86 | 100 |
| 10 | carrier-b | 1 | 100 |
| 10 | carrier-b | 44 | 100 |
| 10 | carrier-b | 81 | 100 |
Profile “profile-international” (international only):
| Priority | Trunk | Prefix | Weight |
|---|---|---|---|
| 10 | carrier-b | 1 | 100 |
| 10 | carrier-b | 44 | 100 |
| 10 | carrier-b | 81 | 100 |
5.4 Create Tenants
| Field | Customer X | Customer Y | Customer Z |
|---|---|---|---|
| Name | CustomerX-Domestic | CustomerY-Full | CustomerZ-International |
| Balance | 50000 | 30000 | 20000 |
| Credit Limit | 5000 | 3000 | 2000 |
| Sell Rate Deck | sell-domestic | sell-domestic + sell-international | sell-international |
| Routing Profile | profile-domestic | profile-full | profile-international |
| Max Concurrent | 200 | 100 | 50 |
| Max CPS | 30 | 15 | 10 |
| LCR | Enabled | Enabled | Enabled |
5.5 Bind Tenant Trunks
Add inbound trunk bindings for each tenant, configuring source IPs:
- Customer X: Source IP
198.51.100.10 - Customer Y: Source IP
198.51.100.20 - Customer Z: Source IP
198.51.100.30
5.6 Hot Reload
Click the “Reload” button on rate decks and routing profiles.
Step 6: Verification
6.1 Test Call
From Customer X’s softswitch, place a call to 861012345678:
- Should match routing prefix
8610(priority 10) - Select carrier-a
- Match sell rate
8610= 0.04 per minute - Match buy rate
8610= 0.025 per minute
6.2 Check CDR
Wholesale → CDR Management: Confirm CDR has been generated with correct fields.
6.3 Check Balance
Tenant Details: Confirm balance has been deducted.
6.4 Price Calculator Verification
Tenant Details → Price Calculator:
- Enter
861012345678→ shows prefix8610, sell 0.04, buy 0.025, profit 0.015 - Enter
12125551234→ Customer X should show “no route” (domestic profile only)
Step 7: Daily Operations
| Operation | Frequency | Location |
|---|---|---|
| Review monitoring dashboard | Daily | Wholesale console |
| Check circuit breaker status | Daily | Monitoring & Throttling |
| Verify balances | Daily | Tenant Details |
| Export CDRs | Weekly | CDR Management → Export |
| Generate invoices | Monthly | Billing Management |
| Rate adjustments | As needed | Rate Decks → Reload |
| Database backup | Daily | MySQLdump / pg_dump |