Privacy Numbers Made Easy with JSON-RPC Routing

M
Miuda Team
Building conversational AI tooling
Rust Telephony SBC CRM Integration SIP

Ride-hailing apps, delivery platforms, and marketplaces all share a common requirement: connect two parties on a phone call without revealing their real numbers. This is the classic privacy number (AX/NXA) use case.

RustPBX’s SBC addon includes a JSON-RPC router that makes this straightforward. Every incoming call can query your backend API, get the real destination, rewrite the numbers, and connect the call – all in real time.

How Privacy Numbers Work

The basic flow:

  1. Caller dials a virtual number (e.g. 17005559999)
  2. RustPBX intercepts the INVITE and calls your HTTP API
  3. Your API looks up the mapping: “17005559999 → real driver number”
  4. RustPBX rewrites the destination and forwards the call
  5. The driver sees the virtual number on caller ID, not the rider’s real number

Both parties are protected. Your platform controls the mapping.

Setting Up JSON-RPC Routing

Enable the Feature

Make sure the SBC addon is active (addons = ["sbc"] in config.toml). The JSON-RPC router is built into the SBC addon – no extra installation needed.

Configure via Console

Go to SBC > JSON-RPC Config in the console. You’ll see a rule editor where you define when and how to call your API.

JSON-RPC config page

Define a Rule

Each rule has three parts: when to match, what to call, and how to handle the response.

Here’s a rule for the privacy number scenario:

# config/sbc/sbc_jsonrpc.toml
enabled = true
timeout_ms = 3000

[[rules]]
name = "privacy-number-routing"
enabled = true

[rules.match_group]
logic = "all"

[[rules.match_group.conditions]]
field = "Callee"
op = "Regex"
value = "^1700555\\d{4}$"

[rules.upstream]
method = "POST"
url = "https://api.yourplatform.com/telephony/resolve"
body = '''{"virtual_number": "{{ call.callee }}", "call_id": "{{ call.call_id }}", "caller": "{{ call.caller }}"}'''

[rules.upstream.headers]
Authorization = "Bearer your-api-key"

[rules.response]
success_when = "json.action == 'forward'"
callee_rewrite = "{{ json.real_number }}"
caller_rewrite = "{{ json.display_number }}"

[[rules.response.inject_headers]]
op = "Set"
name = "Privacy"
value = "id"

[[rules.response.inject_headers]]
op = "Set"
name = "P-Asserted-Identity"
value = "sip:{{ json.display_number }}@{{ call.caller_host }}"

Let’s break this down.

Match Conditions

The match_group defines which calls trigger this rule:

  • Fields: Caller, Callee, CallerUser, CalleeUser, CallerHost, CalleeHost, Direction, CallId, UserAgent, or any SIP header
  • Operators: Equals, NotEquals, Contains, StartsWith, EndsWith, Regex, Exists, NotExists
  • Logic: all (AND) or any (OR)

In our example, we match any call where the callee matches ^1700555\d{4}$ – a virtual number range. You can add more conditions like Direction = "Inbound" or specific trunk filtering.

Calling Your API

When a call matches, RustPBX sends an HTTP request to your upstream endpoint:

POST /telephony/resolve HTTP/1.1
Content-Type: application/json
Authorization: Bearer your-api-key

{
  "virtual_number": "17005559999",
  "call_id": "abc123@192.168.1.1",
  "caller": "14155551234"

The template engine (MiniJinja) uses namespaced variables: {{ call.caller }}, {{ call.callee }}, {{ call.call_id }}, {{ call.direction }}, {{ call.caller_user }}, {{ call.callee_user }}, {{ call.caller_host }}, {{ call.callee_host }}, and any custom extractors you define.

Your API responds:

{
  "action": "forward",
  "real_number": "16505559876",
  "display_number": "17005559999",
  "max_duration": 3600
}

Response Processing

The response section tells RustPBX how to interpret the API result:

  • success_when: A boolean expression. Here, json.action == 'forward' means “proceed if the API says forward” (response fields are available as json.* variables)
  • callee_rewrite: MiniJinja template for the new destination. {{ json.real_number }} maps to the driver’s actual number from the API response
  • caller_rewrite: What the driver sees. {{ json.display_number }} shows the virtual number from the API response
  • inject_headers: Add Privacy: id and P-Asserted-Identity for carrier-level privacy

If the API returns anything else (e.g. action: "reject"), the call is rejected with a configurable status code (default 403).

Extractors for Advanced Matching

Sometimes you need to pull data from SIP headers or URIs. Extractors use regex to capture named groups:

[[rules.extractors]]
name = "caller_user"
source = "Caller"
regex = "sip:(?P<caller_user>\\d+)@"
[[rules.extractors]]
name = "account_id"
source = "header:X-Account-Id"
regex = "(?P<account_id>\\d+)"

These extracted values become available in all templates via {{ extracted.caller_user }}, {{ extracted.account_id }}.

Testing in the Console

Before going live, use the Simulate feature:

  1. Enter a sample caller and callee
  2. The engine shows which rule matches (if any)
  3. Displays the upstream request that would be sent
  4. Shows the expected rewrite result

Route simulation

This lets you verify your match conditions and templates without making real calls.

Multiple Rules

You can chain multiple rules for different scenarios:

  • Rule 1: Virtual numbers in 1700555xxxx → privacy platform API
  • Rule 2: Toll-free 1800xxxxxxx → CRM lookup API
  • Rule 3: Premium rate 1900xxxxxxx → billing verification API

Rules are evaluated in order. First match wins.

What’s Next?

JSON-RPC routing gives you dynamic, API-driven call control. Combined with the trunk and route infrastructure from the previous post, you have a fully programmable SBC.

In the next post, we’ll look at the operational side: how RustPBX helps with SSL certificates, call record archival, and speech-to-text transcription – the day-to-day tasks that keep a telephony platform running.