The flow
- Your backend dials the agent with
create, withrole="agent",record=True, astatus_callback_url, arecording_callback_urland the customer’s number inmetadata. - When the agent’s leg sends
Answered, your app adds the customer withadd_participant, taking the number fromev.metadata. - 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; callingendagain is harmless. - On each
Hangup, your app saves the outcome fromev.roleandev.hangup_cause. - When the recording is ready, your app saves it. The recording event may not arrive for
a call started with
create, so also callget_recordingafter the lastHangup(see Recording).
Start a masked call
Install the SDK and a web server: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
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 toapp.py:
app.py
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:
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: