Skip to main content

Overview

Outbound calls are calls your system initiates - rather than waiting for a caller to ring in. Common use cases:
  • Appointment reminders
  • Sales outreach campaigns
  • Callback queues
  • Alert notifications
Outbound calls use the same Speech Pipe, voice profile, and AgentRunner infrastructure as inbound calls. The difference is you trigger them via the management API.

Prerequisites

  • A configured Speech Pipe with a voice profile - see Speech Pipe
  • A phone number attached to that Speech Pipe - see Numbers
  • A running AgentRunner process for the Speech Pipe

Initiating an Outbound Call

import asyncio
from unpod import AsyncClient

async def main():
    async with AsyncClient(api_key="sk-...") as client:
        call = await client.calls.create(
            pipe_id="pipe_...",
            to_number="+14155550100",    # E.164 format
            from_number="+18005551234",  # optional if a number is attached to the Speech Pipe
            instructions="This is a reminder call for a dental appointment.",
            data={
                "patient_name": "Jane Smith",
                "appointment_date": "2026-06-10",
                "appointment_time": "09:30 AM",
                "doctor": "Dr. Patel",
            },
        )
        print("Call initiated:", call.call_id, call.status)
        # status is "pending" - calls.create() enqueues the call and returns
        # immediately. The call is dispatched asynchronously once the account
        # has free concurrency. Poll GET /calls/{id} (or use hooks) to watch
        # it advance to ringing -> active -> completed. See Call Lifecycle.

asyncio.run(main())
calls.create() is asynchronous: it enqueues the call and returns a record with status="pending". The call is dispatched on a worker as soon as your account has free concurrency. See Call Lifecycle for the full status progression and how to poll for completion.

calls.create() parameters

ParameterTypeRequiredDescription
pipe_idstrYesSpeech Pipe to use for the call
to_numberstrYesDestination number (E.164)
from_numberstrNoCaller ID. If omitted, Unpod uses a number attached to the Speech Pipe
instructionsstr | NoneNoRuntime instruction override for this call
datadict | NoneNoArbitrary metadata passed to your entrypoint via ctx.data

Accessing Outbound Data in Your Entrypoint

The data and instructions you pass to calls.create() are available on the CallContext:
from superdialog import DialogMachine, Flow
from unpod import AgentRunner, CallContext

flow = Flow.load("reminder.json")

async def handle_call(ctx: CallContext) -> None:
    # Available for both inbound and outbound calls
    print(ctx.direction)        # "outbound"
    print(ctx.instructions)     # "This is a reminder call..."
    print(ctx.data)             # {"patient_name": "Jane Smith", ...}

    # Inject context into the dialog machine
    patient_name = ctx.data.get("patient_name", "there")
    appt_date = ctx.data.get("appointment_date", "soon")
    appt_time = ctx.data.get("appointment_time", "")

    ctx.session.dialog_machine = DialogMachine(
        flow=flow,
        llm="anthropic/claude-haiku-4-5",
    )
    # Inject the personalization as an assist directive
    ctx.session.dialog_machine.assist(
        f"You are calling {patient_name}. "
        f"Their appointment is on {appt_date} at {appt_time}. "
        f"Confirm they will attend, or reschedule if needed."
    )

    await ctx.session.run()

runner = AgentRunner(
    entrypoint=handle_call,
    agent_id="my-agent",
)
runner.start()

Checking Call Status

import asyncio
from unpod import AsyncClient

async def main():
    async with AsyncClient(api_key="sk-...") as client:
        call = await client.calls.get("cal_...")
        print(call.call_id, call.status, call.direction)
        # status: "pending", "ringing", "active", "completed", "failed", "cancelled"

asyncio.run(main())

Listing Calls

import asyncio
from unpod import AsyncClient

async def main():
    async with AsyncClient(api_key="sk-...") as client:
        # All outbound calls for a Speech Pipe
        calls = await client.calls.list(
            pipe_id="pipe_...",
            status="completed",
        )
        for c in calls:
            print(c.id, c.to_number, c.status, c.duration_s)

asyncio.run(main())

calls.list() filters

ParameterValuesDescription
statuspending, ringing, active, completed, failed, cancelledFilter by status
pipe_idpipe_...Filter by Speech Pipe

Hanging Up an Active Call

import asyncio
from unpod import AsyncClient

async def main():
    async with AsyncClient(api_key="sk-...") as client:
        await client.calls.hangup("cal_...")
        print("Call terminated")

asyncio.run(main())

Campaign Pattern: Batch Outbound

import asyncio
from unpod import AsyncClient

PIPE_ID = "pipe_..."
FROM_NUMBER = "+18005551234"

contacts = [
    {"name": "Alice", "phone": "+14155550101", "appt": "June 10 9:30 AM"},
    {"name": "Bob",   "phone": "+14155550102", "appt": "June 10 2:00 PM"},
    {"name": "Carol", "phone": "+14155550103", "appt": "June 11 10:00 AM"},
]

async def dial_all(contacts: list[dict]) -> None:
    async with AsyncClient(api_key="sk-...") as client:
        for contact in contacts:
            call = await client.calls.create(
                pipe_id=PIPE_ID,
                to_number=contact["phone"],
                from_number=FROM_NUMBER,
                instructions=(
                    f"Call {contact['name']} to confirm their appointment "
                    f"on {contact['appt']}."
                ),
                data=contact,
            )
            print(f"Dialing {contact['name']}: {call.call_id}")
            await asyncio.sleep(2)  # pace the campaign

asyncio.run(dial_all(contacts))
Batch dialing at high rates may trigger carrier spam filters. Use a sensible interval between calls and comply with local regulations (TCPA in the US, etc.).

Callback Queue Pattern

import asyncio
from unpod import AsyncClient

async def process_callback_queue(queue: list[dict]) -> None:
    async with AsyncClient(api_key="sk-...") as client:
        for request in queue:
            # Check if the Speech Pipe is not at capacity before dialing
            # (AgentRunner handles capacity internally - orchestrator rejects if full)
            call = await client.calls.create(
                pipe_id="pipe_...",
                to_number=request["phone"],
                from_number="+18005551234",
                instructions="This is a callback. The customer requested to be called back.",
                data={"ticket_id": request["ticket_id"], "reason": request["reason"]},
            )
            await db.update_callback_request(
                request["id"],
                call_id=call.call_id,
                status="dialing",
            )

Retrieving Transcripts After a Call

import asyncio
from unpod import AsyncClient

async def get_transcript(call_id: str) -> None:
    async with AsyncClient(api_key="sk-...") as client:
        transcript = await client.transcripts.get(call_id)
        for turn in transcript.turns:
            print(f"[{turn.speaker}] {turn.text}")

asyncio.run(get_transcript("cal_..."))

Next Steps

Hooks & Events

Log outbound call data and react to call events in real time.

Speech Pipe

Configure Speech Pipes and voice profiles for outbound campaigns.