---
title: "Use ThunderPhone from LiveKit Agents"
description: "Run a ThunderPhone voice agent as the realtime (speech-to-speech) model of a LiveKit Agents session, with LiveKit rooms, SIP and telephony around it."
---

[LiveKit Agents](https://github.com/livekit/agents) is an open-source framework
for realtime voice agents on top of LiveKit rooms. ThunderPhone plugs in as the
**realtime model**: the framework sends the participant's audio, ThunderPhone
returns the agent's voice, transcripts and function calls, and LiveKit's room,
SIP trunks and telephony carry the audio. Speech recognition, the language
model, the voice, turn-taking, 47 languages and tools all run on ThunderPhone.

Calls made this way appear in [call history](/api-reference/calls) and are
billed at your product's per-minute rate like any other realtime call. There
is no subscription and no ThunderPhone phone number involved.

## Install

```bash
pip install livekit-plugins-thunderphone
export THUNDERPHONE_API_KEY=sk_live_...   # a secret API key
```

The plugin wraps LiveKit's OpenAI Realtime model, because ThunderPhone's
[Realtime WebSocket](/api-reference/realtime) speaks the same protocol. It
needs `livekit-agents` 1.8 or newer.

<Note>
The plugin is under review in the
[livekit/agents](https://github.com/livekit/agents) repository. Until LiveKit
publishes it, install it from the branch:

```bash
pip install "git+https://github.com/kolchinski/agents@add-thunderphone-plugin#subdirectory=livekit-plugins/livekit-plugins-thunderphone"
```
</Note>

## Run a saved agent

Everything the agent does (prompt, voice, engine, languages, tools, greeting,
silence check-ins) is configured on ThunderPhone. The session only moves audio.

```python
from livekit.agents import Agent, AgentSession, JobContext, WorkerOptions, cli
from livekit.plugins import thunderphone


async def entrypoint(ctx: JobContext):
    session = AgentSession(llm=thunderphone.RealtimeModel(agent_id=12))
    await session.start(agent=Agent(instructions=""), room=ctx.room)


if __name__ == "__main__":
    cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))
```

A saved agent greets and runs its own tools on ThunderPhone; the LiveKit
`Agent`'s instructions and tools are not sent. The call ends when the
ThunderPhone agent hangs up, and the session closes with it.

## Configure the session inline

Without `agent_id`, instructions and tools come from the LiveKit `Agent`, the
same way they do for OpenAI. `model` picks the engine and `voice` picks the
ThunderPhone voice.

```python
from livekit.agents import Agent, AgentSession, RunContext, function_tool
from livekit.plugins import thunderphone


class Receptionist(Agent):
    def __init__(self):
        super().__init__(instructions="You are Acme Dental's receptionist. Be brief.")

    @function_tool
    async def check_availability(self, context: RunContext, date: str) -> dict:
        """Free appointment slots on a date."""
        return {"slots": await calendar.free_slots(date)}


async def entrypoint(ctx: JobContext):
    session = AgentSession(
        llm=thunderphone.RealtimeModel(model="bolt", voice="olivia", language="es"),
    )
    await session.start(agent=Receptionist(), room=ctx.room)
    await session.generate_reply()   # the agent speaks first
```

ThunderPhone freezes instructions and tools when the call starts, so
`update_instructions`, `update_tools` and agent hand-offs that change them are
not supported mid-call. Inline sessions are client-steered: the agent speaks
when you call `generate_reply()` or when the participant finishes a turn.

## Options

| Argument | Meaning |
|---|---|
| `api_key` | Secret key (`sk_live_...`). Defaults to `THUNDERPHONE_API_KEY`. |
| `agent_id` | Run a saved agent. Mutually exclusive with `model` and `voice`. |
| `model` | Engine for inline sessions: `spark`, `bolt` or `storm`. |
| `voice` | ThunderPhone voice name for inline sessions. |
| `language` | Primary language hint for inline sessions, e.g. `es`. |
| `caller_number`, `callee_number` | Phone numbers of the call when the room fronts a phone line. They appear as the from and to numbers in call history. |
| `live_transcripts` | Stream caller transcript fragments mid-utterance (billed extra). |
| `base_url` | Endpoint override, default `wss://api.thunderphone.com/v1/realtime`. |

## Events

ThunderPhone adds platform events on top of the realtime protocol. The session
emits them:

```python
@session.llm.session.on("thunderphone_call_ended")  # or on the RealtimeSession you hold
def on_call_ended(event):
    print("call ended:", event["reason"])
```

The `RealtimeSession` exposes `call_id` once the session is live. Use it with
[`GET /v1/calls/{call_id}`](/api-reference/calls) to fetch the recording,
transcript and grade after the call. Every other `call.*` event (transfer,
keypad, speech ignored) arrives as `thunderphone_call_event`.

## Limits

- Turn detection is server-side and always on; framework-side turn detection
  (`turn_detection` on the `AgentSession`) is ignored.
- Instructions and tools cannot change once the call starts.
- Audio is 16-bit mono PCM at 24 kHz in both directions.
- Video frames are ignored.

<Note>
Building on Pipecat instead? See [Use ThunderPhone from Pipecat](/guides/use-with-pipecat).
</Note>
