<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).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.