Prerequisites
- Python 3.12+
- An Unpod org with a platform token and at least one phone number - provision a number from the dashboard under Dev Platform → Numbers
- The SDK:
.env file in the folder you run the scripts
from:
.env
import unpod loads .env from the current working directory, and
AsyncClient() reads every value from it - no load_dotenv() call needed.
Plain export lines in your shell work too. Values already set in the
shell win over .env.
Token auth (
UNPOD_PLATFORM_TOKEN + UNPOD_ORG_HANDLE) wins over
UNPOD_API_KEY when both are set. Analytics and the telephony calls on
this page need token auth, so use the token.The six steps
agent_id is the only identifier you re-type. The voice name, the
number, and the call_id each cross exactly one step boundary; number_id
and voice_profile_id you never need at all. Every step after the second
takes the agent id and resolves the rest server-side. This page uses
my-support; name yours whatever your team will recognise.
1
Pick a voice profile
A voice profile is not a voice. It is a pre-benchmarked pipeline with a
price tag - an STT model, a TTS provider and voice, a chat model, and the
measured cost and latency of running all three. Choosing one is closer to
choosing an instance type than choosing a ringtone.The
step1_voices.py
Real output, one entry
name is the handle - Riya is what you pass to create() in the next
step.2
Create the agent
You choose both names here, and they do different jobs.
agent_id is the
one string you type again - attaching a number, placing a call, and reading
results all take this exact string. name is the human label, shown in
dashboards, referenced by nobody’s code.Pick the mode for the agent’s brain. Both are managed - Unpod runs the
brain and you deploy nothing:- Prompt mode - the brain is one instruction string. Fastest way to a talking agent.
- Playbook mode - the brain is a published playbook: branches, slots, and tools you build and edit in the Playbook studio.
- Prompt mode
- Playbook mode
step2_agent.py
Output
brain_execution is where the platform runs the brain. It is set for you;
leave it alone unless support asks you to change it.An agent has one brain. Re-running create() with the same agent_id
and the same brain is safe, but switching an existing agent from Prompt to
Playbook (or back) through create() returns 409 Conflict. Change the
brain with client.agents.update("my-support", brain=Playbook("PB_..."))
instead, or pick a new agent_id - see
Configure an agent.Prompt is the shortest path to something that answers: the brain is a bare
instruction string, the platform runs it as a one-node playbook, and there is
nothing to deploy or publish first. One thing in that prompt is doing more work than it looks:
the two-sentence cap. On a phone line, reply length is a latency setting,
not a style preference.Things to know about create():recordingis off by default. Turn it on now; you cannot recover a call you did not record.greetingoverrides the profile’s own greeting line.max_call_duration_sdefaults to 3600, andmax_concurrentdefaults to 1 - raise it before any load test, or your second caller waits.agents.voice.add(agent_id, voice_profile=...)gives the same brain a second voice, because the brain lives on the agent.
3
Find a free number
List first - you can only attach a number the org holds and that nothing
else has claimed.Each row has two fields,
step3_numbers.py
Output
number (E.164) and status:telephony.numbers.list() takes no filters; filter the list in code as
above. If nothing is free, provision a number from the dashboard under
Dev Platform → Numbers, or detach one from another agent.4
Attach it to the agent
This script is self-contained: it finds the first free number and attaches
it, so you never copy a number by hand.
step4_attach.py
Output
status=assigned confirms the binding. To attach a specific number,
pass it directly: await client.agents.numbers.attach("my-support", "+9180...").From here you never pass the number again - calls.create() resolves it
from the agent. To move the number to a different agent later, use
client.agents.update("other-agent", number="+9180...") - see
Configure an agent.5
Place the call
The minimal call is three pieces of information: the agent, the destination,
and Three things people expect to pass, and should not:
data - the per-call context the brain can read. It is what turns
“greet the caller by name if you know it” from a wish into an instruction.step5_call.py
- No
from_number. Caller ID resolves from the number attached to the agent. - Nothing about the voice. The agent already knows which profile it speaks with and which brain answers.
- The result is the queued state, not a finished call. The platform enqueues and returns immediately.
A
from_number of None on the queued record means no number is attached
to the agent, and the dial has nothing to originate from - go back to
step 4.6
Read the result
calls.create() is asynchronous, so you poll until the call reaches a
terminal state - and a call reaches one in two different ways, both of
which you have to check. ended_at is stamped when a connected call hangs
up, but a call that never started - busy, no answer, rejected - finishes
without ever being stamped. Poll on ended_at alone and a busy signal hangs
your script for the full budget.step6_result.py
calls.get() is the whole result: status, end reason, disposition,
duration, recording URL, and the transcript turn by turn. If you want the
orchestration run behind it - room, participants, usage -
client.sessions.get(call.session_id) reads that, and the session_id is
what support will ask for if something looked wrong.
calls.hangup(call_id) ends a live call from your side.Good to know about reading calls
calls.list()leaves transcripts out so a page of 500 calls stays small: on a list rowtranscriptisNone(not loaded), as distinct from[](the call genuinely said nothing). Every row still carriestranscript_turns. Uselistto find the call,getto read it.- Want typed fields instead of raw transcripts? Attach an analytics block to
the agent - an extraction schema that runs on every completed session, read
back with
client.analytics.list_results(agent_id="my-support", limit=100). Attach it before your first call: blocks run forward and never backfill.
Which brain
Everything above usedPrompt because it deploys nothing. All four brains
sit in the same loop, behind the same voice - the real question is how much
you are willing to run. Swapping costs one line:
Before production
Six settings and one habit. Defaults are tuned for a first call, not for a campaign.Troubleshooting
httpx.ConnectError(Name or service not known/getaddrinfo failed) - the host inUNPOD_BASE_URL/UNPOD_SERVICE_BASE_URLdoes not exist. Set both tohttps://unpod.ai(or the host your team gave you).404, or an HTML page instead of JSON -UNPOD_SERVICE_BASE_URLis missing or has a path on it. It must be the bare host, e.g.https://unpod.ai.401 Unauthorized- the token is wrong orUNPOD_ORG_HANDLEdoes not match the org the token belongs to.ValueErrorasking forUNPOD_PLATFORM_TOKEN- the SDK found no credentials. Check that.envsits in the folder you runpythonfrom.409 Conflictfromagents.voice.create()- thatagent_idalready exists with a different brain. Useagents.update()or a new id.- The call stays
pendingandfrom_numberisNone- no number is attached to the agent. Re-run step 4 and check the attach response. - Your script hangs on a busy line - you are polling
ended_atalone. A never-connected call finishes by terminal status only; use thewait_for_call()pattern from step 6. transcriptisNone- on a list row it means not loaded (usecalls.get()); on a never-connected call there are simply no turns.- No
not_assignednumber in step 3 - provision one from the dashboard under Dev Platform → Numbers, or detach one from another agent.
Next
Configure the agent
Change voice or number, background sound, noise cancellation, and more.
Handover & follow-up
Transfer to a human, voicemail messages, automatic callbacks.
Domain dictionaries
Teach the agent your product names and how to say them.
Run your own brain
A
Runner at the text boundary - your process, your framework.What is a playbook
Branches, slots, and tools instead of one long prompt.
Talk to it in the browser
The same agent over a browser session - no number involved.