Skip to main content
Connect two parties, a field agent and a customer, through masking numbers so neither ever sees the other’s real number. Either side can call, and both directions are private. XML elements used: <Dial callerId=…>, <Number>, <Speak>, <Hangup>

View on GitHub

Clone and run the full working example
New to the concept? Read What is Number Masking? for the why and the call-flow theory, then come back here to build it.

Getting started

Overview

You provision two masking DIDs and attach both to a single Vobiz application. When either number is dialed, Vobiz POSTs the call to your /answer webhook. Your backend looks up the dialed number (To), checks the caller (From) is authorized, and returns a <Dial> that bridges to the real destination, showing the masking number as caller ID. Neither party sees a real number. The whole system is two rows of mapping:
  • M1 is the number the agent dials to reach the customer.
  • M2 is the number the customer dials to reach the agent.
Caller IDs are cross-mapped so each party always sees one consistent number for the other: dial M1 and the customer sees M2; dial M2 and the agent sees M1.

Call flow

If the caller is not the registered party for that masking number, the backend rejects the call with a spoken message instead of bridging.

Vobiz webhook

Set /answer as the Answer URL on your Vobiz application (the included setup_live.py does this for you).

Environment variables

Step 1: The number map

All numbers come from the environment, so no real numbers live in source. The resolve() function returns a Leg only when the dialed number is a known masking number and the caller is the registered party for it. Matching is format-tolerant (compares the final 10 digits), so +, spaces, a leading 0, or a missing country code all still match.
mappings.py

Step 2: The XML builders

vobiz_xml.py holds a few small helpers (response(), dial(), speak(), hangup()) that return Vobiz XML strings so handlers don’t concatenate strings by hand. The only one that matters for masking is dial(), whose caller_id argument becomes the callerId the called party sees, and for masking that is always the masking number, never the real caller:
The full builder module is in the repo, you can also just return the XML as a literal string if you prefer.

Step 3: The webhook

The /answer handler is stateless and idempotent: it reads From and To, resolves the mapping, and returns either a bridging <Dial> or a spoken rejection. Every request carries everything it needs, so retries are safe and you can run many instances behind a load balancer.
app.py
When the agent dials M1, this returns:

Step 4: Expose your server over HTTPS

Vobiz needs a public HTTPS Answer URL. In development, tunnel your local port:
Copy the https://… URL it prints into PUBLIC_BASE_URL in your .env.

Step 5: Go live

Run the bundled one-time setup script:
It creates a Vobiz application whose Answer URL is PUBLIC_BASE_URL/answer, then attaches both masking DIDs to it, so inbound calls to either number hit your server. (You can also do this in the dashboard: create one application and point both numbers at it.)

Test it

Offline (no real call), simulate Vobiz’s webhook with a POST:
Expected response, dial the customer, show M2 as caller ID:
An unregistered caller is rejected:
Then place a real call: from the agent phone, dial M1, the customer’s phone should ring showing M2.

Troubleshooting

The callerId on <Dial> must be a Vobiz DID you own. Confirm leg.caller_id_shown resolves to M1/M2 (your masking numbers), not the real FE/CUSTOMER values. Using a number you don’t own is rejected as Unknown Caller ID (hangup code 3030, see Hangup Causes).
resolve() requires the From number to match the registered caller for that masking number. Check the caller is dialing from their registered handset and that FE/CUSTOMER in .env match the real numbers (matching uses the last 10 digits, so country-code formatting is tolerant but the digits must match).
Verify PUBLIC_BASE_URL is reachable over HTTPS and that setup_live.py attached the application to both DIDs. A non-2xx or unreachable Answer URL fails with code 7011; an invalid XML response fails with 8011, see Hangup Causes.
Your /answer handler must respond well within the webhook timeout (aim for sub-second). Keep the mapping in fast in-memory or cache storage and avoid slow DB calls on the hot path.

What’s next

  • Expire mappings. Create a pairing when a transaction starts and remove it when the order is delivered or the ride ends, after that, dialing the masking number connects to nothing.
  • Scale beyond one pair. Reuse a pool of DIDs across many simultaneous pairings by keying the mapping on (masking number + caller) and rotating numbers.
  • Add recording or IVR. Compose the masked leg with call recording or a cloud IVR greeting before bridging.
  • Validate callbacks. Verify webhook authenticity before acting on a request in production.

Number masking, the use case

See where two-way masking fits, ride-hailing, delivery, marketplaces, and how Vobiz powers it.