Skip to main content
Every call - whether you start it or a caller rings in - ends up the same way: a Unpod media worker and the caller are the two participants in a LiveKit room, and the worker bridges the audio while streaming transcripts to your AgentRunner over a separate text-only bridge. Your dialog brain never joins the room or touches audio. This page traces both directions, the states a call moves through, and how it can end.
Animated call lifecycle diagram showing a caller entering Unpod, speech converted to text, dispatch to AgentRunner and Session, dialog turns, reply text, and final transcript, metrics, and webhook storage.

The state machine

Inbound calls skip pending and dialing - the caller is already on the line, so the call starts at ringing/active. Only outbound calls pass through the full sequence. You never drive these states yourself; you observe them via the call status, the call_end hook, and the final end_reason.

Outbound: what happens after calls.create()

calls.create() is asynchronous. It does not wait for the phone to ring - it puts your call on a queue and returns immediately with status: "pending". The actual dialing happens in the background, gated by your plan’s per-account concurrency limit.
1

You enqueue the call

calls.create(pipe_id, to_number, ...) records the call and returns 201 with status: "pending". Your request is never blocked on the network.
2

Unpod gates on concurrency

A background worker picks up the call. If your account is at its concurrent-call cap, the call is automatically rescheduled and retried shortly - no error, no dropped call.
3

Unpod dials the number

Unpod creates a room and originates the SIP call to to_number. The call row moves to ringing, and you get a session_id.
4

Your agent joins and talks

A Unpod media worker joins the room and bridges the caller’s audio; transcripts stream to your AgentRunner over its text bridge and reply text comes back for TTS. The call row moves to active, then completed on hangup.
Because create() returns pending, poll client.calls.get(call_id) to watch the status advance. Don’t assume the call is connected the moment create() returns.

Inbound: what happens when someone calls your number

Inbound is simpler - there is no queue, because there is nothing to rate-limit. The moment a caller dials a number attached to your Speech Pipe, the call is already live and Unpod connects your agent to it.
1

Caller dials your number

The carrier delivers the call over SIP. LiveKit answers, creates a room, and adds the caller as a participant.
2

Unpod resolves your pipe

Unpod matches the dialed number to your Speech Pipe and its voice profile.
3

Your agent is bridged to the live call

A Unpod media worker joins the room the caller is already in and bridges the audio; transcripts start streaming to your AgentRunner over its text bridge immediately.

The audio + transcript path

Once both legs are in the room, every call works the same way. Unpod’s speech stack transcribes the caller and streams plain text to your AgentRunner; your dialog logic replies with text; Unpod synthesizes it back to speech. The animated lifecycle above is the same loop in motion: call audio stays on Unpod’s side, text crosses into your runner, and reply text comes back for TTS.
Your agent_id selects which dialog brain runs for the call - it is not how Unpod routes the phone number. Number routing is handled by the Speech Pipe the number is attached to.

Voicemail detection

Unpod automatically detects when an outbound call has reached a voicemail system so your agent does not waste a turn talking to a machine. There are two detection points:
1

Pre-connect (during ringing)

Many carriers route “forwarded to voicemail” to a SIP user-unavailable signal after a brief ring. When Unpod sees this pattern, it classifies the call as voicemail before it ever connects, ends the call, and reports end_reason: "VOICEMAIL_DETECTED_PRECONNECT".
2

Post-connect (just after answer)

Some voicemail systems answer, play a greeting, then go silent. If a call connects, the caller never speaks, and the leg ends within a short window, Unpod classifies it as voicemail and reports end_reason: "AGENT_HUNG_UP_VOICEMAIL_DETECTED".

End reasons

When a call finishes, Unpod records why. These are the reasons you are most likely to act on:
This is not the full set - additional reasons exist for SIP/trunk configuration errors and handover edge cases. Treat unknown reasons as a generic failure and log them.

Reacting to outcomes in your agent

Observe telephony outcomes through hooks rather than polling: