Prompt
brain and deployed nothing. This page is the Runner path: your own
long-lived Python process at the text boundary - for DB reads mid-call, your
own framework, or logic a prompt cannot carry.
The deal is unchanged: your
AgentRunner receives a
transcribed turn as a string and returns a string. No frames, no codecs, no
SIP - the speech pipeline and the carrier stay on Unpod’s side.
Prerequisites
- Python 3.12+ and
pip install unpod UNPOD_API_KEY(sk_...) in the environment - the runner’s bearer, from unpod.ai/api-keys- A number in your account. Provision one from the dashboard under Dev Platform → Numbers. No carrier account, no SIP trunk - see Numbers.
Already own a number elsewhere? You can bring it over a
trunk instead. That is the only path that needs
carrier credentials.
Step 1 - Provision the number
Provisioning uses the Management API - the REST half of the SDK, reached throughAsyncClient. For REST auth it accepts either the org-scoped
UNPOD_PLATFORM_TOKEN (with UNPOD_ORG_HANDLE) or UNPOD_API_KEY; this
page uses UNPOD_API_KEY, since the runner needs it anyway. The REST
endpoint derives from UNPOD_BASE_URL (default api.unpod.ai →
https://<host>/platform).
Provisioning is the same agents.voice.create() call as the
Quickstart - the only difference is
brain=Runner(): your process answers the turns instead of a platform-run
prompt.
If you need one-off overrides in code, pass base_url= to AsyncClient or
AgentRunner; those arguments win over .env for that process only.
Run this once to pick a voice profile, create the agent, and attach a free
number from your account:
voice_profiles.list()- pick a voice from the read-only catalog.agents.voice.create()- create the agent, its brain, and its first voice.numbers.list()- find a number in your account with nothing attached.numbers.attach()- route that number to theagent_id. It takesagent_id, not a pipe id.
Bringing your own number instead
Bringing your own number instead
The agent’s
agent_id ("my-runner-agent") is the same string your
AgentRunner registers under in Step 2. They must match exactly, or
inbound calls never reach your runner. See
IDs You’ll Meet.Step 2 - Answer an inbound call
Oneentrypoint, one AgentRunner. The brain here is SuperDialog’s
LLMAgent; any adapter works - Anthropic, OpenAI, LangChain, an HTTP
endpoint, or a playbook (see
Bring your agent) - and nothing about the
call path changes:
agent_id it is attached
to, and dispatches the call to your waiting runner. Your agent answers
and speaks - your own process at the text boundary, live on a phone call.
The runner does not need to restart when you provision the number. Run
setup.py
once, then leave the runner up; it serves every inbound call until you stop it.If the runner will not connect
Three close codes cover almost every failure:- 4001 on the control socket - the orchestrator refused the credential. Do not retry: a reconnect loop on a bad key is indistinguishable from a network outage.
- 4003 on the bridge - the pairing failed: an unknown
call_id, an expired pairing (it expires ~20s after dispatch), or a bad call token. - 4009 - that call already has a live bridge socket.
Step 3 - Make an outbound call (optional)
Inbound is one direction. To have your agent place a call, usecalls.create
with the agent_id and the destination number:
calls.create enqueues the call and returns immediately with status="pending".
Unpod dials out, then dispatches the answered call to the same running
AgentRunner - your entrypoint handles outbound exactly as it handles inbound.
calls.create also still accepts pipe_id= for older code. agent_id wins if
you pass both, and to_number is always required. Outbound dispatch may also
need from_number= depending on your numbers - see
Outbound calls.Next steps
Production setup
The full path: trunks, numbers, recording, and deployment.
Outbound calls
Campaigns, dynamic instructions, and per-call data.
Use your own agent
Plug in LangChain, an HTTP endpoint, or any brain you already have.