<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:
M1is the number the agent dials to reach the customer.M2is the number the customer dials to reach the agent.
M1 and the customer sees M2; dial M2 and the agent sees M1.
Dialed number (To) | Allowed caller (From) | Bridges to | Caller ID shown |
|---|---|---|---|
M1 | the agent | the customer | M2 |
M2 | the customer | the agent | M1 |
Call flow
Vobiz webhook
Set/answer as the Answer URL on your Vobiz application (the included setup_live.py does this for you).
| Method | Path | Description |
|---|---|---|
| POST | /answer | Resolves the dialed masking number and returns a <Dial> (or a rejection) |
| GET | /health | Health check |
Environment variables
| Variable | Required | Description |
|---|---|---|
VOBIZ_AUTH_ID | Yes | Vobiz account auth ID |
VOBIZ_AUTH_TOKEN | Yes | Vobiz account auth token |
VOBIZ_API_BASE | No | API base (defaults to https://api.vobiz.ai/api/v1) |
PUBLIC_BASE_URL | Yes | Your public HTTPS tunnel (Cloudflare/ngrok), used as the Answer URL |
M1 | Yes | Masking DID the agent dials → reaches the customer |
M2 | Yes | Masking DID the customer dials → reaches the agent |
FE | Yes | Field agent’s real number |
CUSTOMER | Yes | Customer’s real number |
Step 1: The number map
All numbers come from the environment, so no real numbers live in source. Theresolve() 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:
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
M1, this returns:
Step 4: Expose your server over HTTPS
Vobiz needs a public HTTPS Answer URL. In development, tunnel your local port:https://… URL it prints into PUBLIC_BASE_URL in your .env.
Step 5: Go live
Run the bundled one-time setup script: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 aPOST:
M2 as caller ID:
M1, the customer’s phone should ring showing M2.
Troubleshooting
The call connects but the recipient sees a real number, not the masking number
The call connects but the recipient sees a real number, not the masking number
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).Every call is rejected as 'not authorised'
Every call is rejected as 'not authorised'
Vobiz never hits my webhook
Vobiz never hits my webhook
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.The bridge takes too long or drops
The bridge takes too long or drops
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.