Skip to main content
Let two people talk without sharing their phone numbers: a delivery agent and a customer, a support agent and a caller, a driver and a rider. Unpod dials both from your Unpod number, so each one sees only that number, never the other’s. A masked call is a call with two participants. If you haven’t added a second person to a call yet, read Multiple Participants first. This page is the production version: everything happens on events, and the call ends as soon as either person is gone.

The flow

  1. Your backend dials the agent with create, with role="agent", record=True, a status_callback_url, a recording_callback_url and the customer’s number in metadata.
  2. When the agent’s leg sends Answered, your app adds the customer with add_participant, taking the number from ev.metadata.
  3. When the customer’s leg ends, your app ends the call, whether the customer’s phone was busy (Busy Line), rang out (Ring Timeout Reached, No Answer), rejected the call (Rejected) or they hung up after talking. Otherwise the agent waits on the line alone. When the agent’s leg ends, Unpod already hangs up the customer; calling end again is harmless.
  4. On each Hangup, your app saves the outcome from ev.role and ev.hangup_cause.
  5. When the recording is ready, your app saves it. The recording event may not arrive for a call started with create, so also call get_recording after the last Hangup (see Recording).

Start a masked call

Install the SDK and a web server:
Export your credentials (UNPOD_BASE_URL, UNPOD_PLATFORM_TOKEN, UNPOD_ORG_HANDLE) and your webhook secret as UNPOD_WEBHOOK_SECRET. Your server needs a public https URL (ngrok works, as in the Quick Start). Start app.py with:
app.py
start_masked_call is a plain function, not a route. Call it from your own backend, for example when an order is assigned to a delivery agent. A public route that starts calls would let anyone dial from your number. The same request over REST:
cURL
Outbound events don’t carry the phone numbers in ev.to or ev.from_, so the customer’s number travels in metadata, which Unpod echoes on every event as ev.metadata.
Whether a call is recorded follows the recording policy (set by the Unpod team): record=True doesn’t override it today. See Recording.

Handle events

Add the two routes below to app.py:
app.py
Run it with python app.py and expose it as in the Quick Start. Then start a call from another terminal (ngrok already holds the second one), in the same directory and virtualenv, with all four variables (UNPOD_BASE_URL, UNPOD_PLATFORM_TOKEN, UNPOD_ORG_HANDLE, UNPOD_WEBHOOK_SECRET) exported, since importing app reads them:
The agent’s phone rings first, showing your Unpod number. When they answer, the customer’s phone rings, also showing your Unpod number, and the two are connected. Every event for the call has the same ev.call_id; ev.leg_id tells the two legs apart and ev.role is the role you gave each one. Until it gets a 2xx, Unpod resends an event a few times over about a minute (Delivery and retries), so every branch is safe to run twice: the idempotency_key stops a resent Answered from dialing the customer again, and ending a call that’s already over succeeds again (a 404 comes back only briefly right after hangup, and is logged and ignored). A rate limit or server error answers 503, so Unpod sends the event again; any other error adding the customer ends the call, so the agent isn’t left waiting. If an Answered or Hangup is lost for good, someone can stay on the line: as a backstop, sweep your active calls with calls.list() (every page, until one comes back empty) every few minutes and end any that should be over: one with only one leg left after the customer’s ring time (see calls.get(call_id).legs), or one older than your longest expected call. In the same sweep, clear the ours and outcomes entries of calls that are over and saved, so they don’t grow forever. The /recording route doesn’t check a signature, so it ignores any call_id your /call-status route hasn’t seen on a signed event (which also skips a resent event once the recording is saved), then asks the API for the recording with your credentials. You can also put a secret token in the query string of recording_callback_url: Unpod sends it back unchanged. Save the call_id and duration; to download the audio, see Recording. The recording event may not arrive for a call started with create, so after the last Hangup also call get_recording yourself. Run it from a background task (FastAPI’s BackgroundTasks, or your job queue), not inside /call-status: the handler must answer with a 2xx quickly, within 10 s.

Event map

Store any other cause (Service Unavailable, Unknown, …) and ev.sip_code as it is; see Hangup causes. A call holds at most two people, so a masked call is always one agent and one customer.

Customer calls the masked number

The other direction: the customer calls your Unpod number back and you connect them to their agent. Point the number’s answer URL at your app (see Incoming Calls), look up the agent from the caller’s number (ev.from_ on StartApp) in your own data, and reply:
If several pairings share one Unpod number, the caller’s number alone can match more than one agent: give each pairing its own number, or ask the caller for an order ID first. For the full server, with signature checks and a busy reply, see Handle the call.
Masking an inbound call with <Dial> is not verified to hide the caller’s number: the agent may see the customer’s real number. Test it with your own phones before relying on it.