Skip to main content
You built and talked to your agent in the browser in the Quickstart. Now give it a phone number. Nothing about the agent changes - same entrypoint, same LLMAgent brain, same AgentRunner against Unpod’s hosted speech service. The only new work is provisioning: telling Unpod which phone number routes to your agent.

What you’ll add

  • A Speech Pipe - the configuration entity that binds a voice profile, recording settings, and the agent_id that points at your runner.
  • A phone number - synced from a trunk and attached to that pipe.
You reference a voice profile from the read-only catalog when you create the pipe; you do not create one.

Provision the number

Provisioning uses the Management API - the REST half of the SDK, reached through AsyncClient. It reads UNPOD_API_KEY and derives its REST endpoint from UNPOD_BASE_URL (https://<host>/platform) - both set in the Quickstart. 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 pipe, sync numbers from your trunks, and attach the first available number:
# setup.py - run once to provision your phone number
import asyncio
from unpod import AsyncClient

async def setup() -> None:
    async with AsyncClient() as client:
        # 1. Pick a voice profile from the read-only catalog.
        profiles = await client.voice_profiles.list(language="en")
        if not profiles:
            print("No voice profiles found.")
            return
        vp = profiles[0]
        print(f"Using voice profile: {vp.name} ({vp.profile_id})")

        # 2. Create the Speech Pipe. agent_id MUST match your AgentRunner.
        pipe = await client.pipes.create(
            name="browser-agent",
            voice_profile=vp.name,        # name (case-insensitive) or profile_id
            agent_id="browser-playground",  # must match AgentRunner's agent_id
            recording=True,
            max_call_duration_s=600,
        )
        print(f"Created Speech Pipe: {pipe.pipe_id}")

        # 3. Sync numbers from your trunks.
        summary = await client.numbers.sync()
        print(f"Sync result: {summary}")  # {"synced": int, "new": int}

        # 4. Attach the first available number to the pipe.
        available = await client.numbers.list(status="available")
        if not available:
            print("No available numbers - register a trunk first.")
            return
        number = await client.numbers.attach(available[0].number_id, pipe.pipe_id)
        print(f"Attached {number.number} -> Speech Pipe {pipe.pipe_id}")

asyncio.run(setup())
The agent_id on the pipe ("browser-playground") is the same string your AgentRunner registers under in the Quickstart. They must match exactly, or inbound calls never reach your runner. See IDs You’ll Meet.

You need a trunk first

numbers.sync() pulls numbers from your trunks - the SIP connections that carry calls between a telephony provider and Unpod. If numbers.list(status="available") comes back empty, you have no trunk yet. Register one with client.trunks.create(...) before running the setup above. You configure a trunk once; after that you work with numbers, not SIP. See Trunks for both trunk types, or the Setup Checklist for the full production path.

Answer an inbound call

This is the agent from the Quickstart, unchanged. The same entrypoint and the same AgentRunner registered against wss://<UNPOD_BASE_URL>, using your real UNPOD_API_KEY:
# agent.py - standalone runner, no playground server needed
import asyncio
import os
from unpod import AgentRunner, CallContext
from superdialog import LLMAgent

async def entrypoint(ctx: CallContext) -> None:
    ctx.session.dialog_machine = LLMAgent(
        llm="anthropic/claude-haiku-4-5-20251001",
        system_prompt="You are a helpful voice assistant. Keep answers under 3 sentences.",
    )
    await ctx.session.run()

def build_runner() -> AgentRunner:
    return AgentRunner(
        entrypoint=entrypoint,
        agent_id=os.getenv("AGENT_ID", "browser-playground"),
        # base_url and api_key derive from UNPOD_BASE_URL / UNPOD_API_KEY
        # unless you pass explicit args here.
    )

async def run_agent() -> None:
    await build_runner().run()

if __name__ == "__main__":
    asyncio.run(run_agent())
Start the runner:
python agent.py
The runner connects to the orchestrator and waits. Now call the number you attached. Unpod recognises the number, looks up its pipe, sees the pipe’s agent_id, and dispatches the call to your waiting runner. Your agent answers and speaks - the same brain you heard in the browser, now on the phone.
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.

Make an outbound call (optional)

Inbound is one direction. To have your agent place a call, use calls.create with the pipe and the destination number:
# call_out.py - dispatch an outbound call
import asyncio
from unpod import AsyncClient

PIPE_ID = "pipe_..."   # from setup.py
TO_NUMBER = "+19995550001"

async def make_call() -> None:
    async with AsyncClient() as client:
        call = await client.calls.create(
            pipe_id=PIPE_ID,
            to_number=TO_NUMBER,
        )
        print(f"Outbound call {call.call_id} -> {TO_NUMBER} (status: {call.status})")

asyncio.run(make_call())
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.

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.