# Python

> Voice SDK reference for Python. Originate and control calls, then bridge audio over QUIC/MoQT.

The Python Voice SDK gives your backend two surfaces against FirstByte
Voice. The **control plane** (`voice.calls` / `voice.agents`) originates,
transfers, and hangs up calls, and binds server-side agents over the
control-plane API. The **data plane** (`voice.audio_bridge`) streams call
audio in and out over our QUIC/MoQT transport. Use it when the call driver
lives on a server rather than in a browser — an outbound dialer, an agent
supervisor, or a "tap the audio to my own ASR" pipeline.

> **NOTE:**
> The control plane is pure Python (standard-library HTTP, no native
>   dependency). The **data plane** (`voice.audio_bridge`) uses our QUIC/MoQT
>   transport through a shared native library. To publish or subscribe to audio,
>   the MoQT FFI must be available at runtime (see [Install](#install-and-authenticate)).
>   The SDK is **synchronous and blocking**. It does not use `asyncio`. Uplink
>   frames arrive on a callback you register.

## Install and authenticate

  1. **Install the package**
```bash
    pip install clutchcall-sdk
    ```
  2. **Provide the audio transport library (data plane only)**
Originating and controlling calls works with the package alone. To stream
    audio through `voice.audio_bridge`, point the SDK at the native MoQT
    transport library and export your API key:

    ```bash
    export CLUTCHCALL_MOQT_FFI=/path/to/libclutchcall_moqt_ffi.so
    export CLUTCHCALL_API_KEY=...   # your Voice API key
    ```

    > **TIP:**
> Skip the library if you only originate calls and let a server-side agent
>       handle the audio (`agent=...` on `originate`, or `voice.agents.attach`).
>       The engine wires the bridge for you in that case.
  3. **Construct the client**
```python
    import os
    from clutchcall.voice import Voice

    v = Voice(
        base_url="https://engine.firstbyte.fast",
        api_key=os.environ["CLUTCHCALL_API_KEY"],
        org_id="org_abc",
    )
    ```

## `Voice(...)`

Top-level client. All constructor arguments are keyword-only.

- **`base_url`** (`str`, required) — Control-plane API base URL for your workspace, for example `https://engine.firstbyte.fast`.
- **`api_key`** (`str`, required) — Voice API key (sent as a bearer token). Keep it server-side.
- **`org_id`** (`str`, required) — Your workspace / organization id. It scopes every request.
- **`relay_host`** (`str`, default `relay.firstbyte.fast`) — MoQT relay host the audio bridge dials for the data plane. Override only for   self-hosted / on-prem deployments.

The client exposes three helpers:

| Attribute | Type | Plane |
| --- | --- | --- |
| `v.calls` | `Calls` | control — originate / get / transfer / hangup |
| `v.audio_bridge` | `AudioBridgeFactory` | data — attach and stream audio |
| `v.agents` | `Agents` | control — bind a server-side agent to a call |

Every surface raises `VoiceError` (importable from `clutchcall.voice`) on a
bad argument or a non-2xx control-plane response.

## Originate and control calls

### `v.calls.originate(...)`

Places an outbound call against a trunk and returns a `Call`. Arguments are
keyword-only.

- **`to`** (`str`, required) — Destination in E.164, e.g. `"+15551234567"`.
- **`from_`** (`str`, required) — Caller-ID in E.164. (The name is `from_` because `from` is a Python keyword.   The SDK sends it as `from` on the wire.)
- **`trunk_id`** (`str`, required) — The SIP trunk to originate on. See   [SIP trunking](/modalities/voice/transport-telephony/sip-trunking).
- **`agent`** (`str | None`, default `None`) — If set, the engine binds this server-side agent and wires the audio bridge   end-to-end. You do not need to open an `AudioBridge` yourself.
- **`ring_timeout_sec`** (`int`, default `30`) — The number of seconds to ring before the attempt stops.

```python
call = v.calls.originate(
    to="+15551234567",
    from_="+15558675309",
    trunk_id="trunk_main",
    agent="healthcare-assistant",   # optional: engine drives the audio
    ring_timeout_sec=30,
)
print(call.sid, call.status)
```

### The `Call` object

`originate` and `get` return a `Call`. Its `sid` is the universal key. The same
`call_sid` names the SIP dialog, the media session, the agent session, the MoQT
namespace, and the CDR.

- **`sid`** (`str`) — Call identifier (`call_sid`).
- **`status`** (`str`) — One of `dialing`, `ringing`, `in_progress`, `completed`, `failed`, `no_answer`.
- **`to`** (`str`) — Destination E.164.
- **`from_`** (`str`) — Caller-ID E.164.
- **`trunk_id`** (`str | None`) — Originating trunk.
- **`agent`** (`str | None`) — Bound agent, if any.

**Methods:**

```python
# Fetch current state for a known sid
call = v.calls.get(sid="call_...")

# Transfer — pass EXACTLY ONE of to (a PSTN number) or agent (re-attach)
call.transfer(to="+15557654321")
call.transfer(agent="tier2-support")

# End the call
call.hangup()
```

> **WARNING:**
> `transfer` requires exactly one of `to=<E.164>` or `agent=<id>`. If you pass
>   both, or neither, the SDK raises `VoiceError`.
>   [AI ↔ human handoff](/modalities/voice/transport-telephony/ai-human-handoff)
>   covers peer-initiated transfer over SIP `REFER`.

### `v.agents.attach(call_sid, agent)`

Bind a running server-side agent to an existing call. The engine wires the
audio bridge for you. Use this when the call already exists (for example, an
inbound call) and you want the agent runtime to take the audio.

```python
v.agents.attach(call.sid, "healthcare-assistant")
```

## Bridge call audio

`voice.audio_bridge.attach(...)` opens a bidirectional audio bridge for one
call. It **subscribes to the caller's uplink** (`voice/<sid>/uplink`) and
delivers each frame to your `on_uplink` callback. It also **publishes your
downlink** (`voice/<sid>/downlink`) back toward the caller through
`bridge.publish_downlink(...)`. This is the server / agent side of the bridge:
you receive caller audio and send audio to the caller.

- **`call_sid`** (`str`, required) — The `call.sid` to bridge.
- **`on_uplink`** (`Callable[[bytes, int], None]`, required) — The SDK calls this for every caller frame with `(frame_bytes, timestamp_us)`.   It runs on the transport's delivery thread. Keep it fast and hand off heavy   work.
- **`codec`** (`"opus" | "pcm16" | "g711_ulaw" | "g711_alaw"`, default `"opus"`) — Wire codec for the downlink you publish. See [Codecs](#codecs-and-audio-format).
- **`sample_rate`** (`int`, default `48000`) — Sample rate of the audio you publish (48 kHz for Opus).
- **`channels`** (`int`, default `1`) — Channel count (mono).
- **`frame_ms`** (`int`, default `20`) — Frame duration in ms.

```python
def on_uplink(frame: bytes, ts_us: int) -> None:
    my_asr.feed(frame)          # caller audio → your pipeline

bridge = v.audio_bridge.attach(call.sid, codec="opus", on_uplink=on_uplink)

# Send audio back to the caller
for opus_frame in my_tts.stream():
    bridge.publish_downlink(opus_frame)

# Release when done — this closes the MoQT session and drops both tracks
bridge.close()
```

`AudioBridge.publish_downlink(frame, *, timestamp_us=None)` sends one frame
toward the caller. If you omit `timestamp_us`, the SDK stamps it from a
monotonic clock. Hold the `AudioBridge` reference for the call's lifetime. If
the garbage collector collects it, the session closes.

> **NOTE:**
> The Python data plane today ships the **server/agent-side** attach only
>   (subscribe uplink, publish downlink). The caller-side helpers exposed by the
>   TypeScript SDK — `attachCaller`, `publishUplink`, `onDownlink` — are **not yet
>   part of the Python surface**. If you need to originate audio *as the caller*
>   from Python (for example, a synthetic caller), use the
>   [TypeScript SDK](/modalities/voice/sdk/javascript) for now.

## Codecs and audio format

`codec` accepts `"opus" | "pcm16" | "g711_ulaw" | "g711_alaw"`. Voice is
audio-only. There are no video codecs. Phone legs are G.711 (µ-law / A-law) on
the wire. Browser and app legs are Opus (48 kHz mono, 20 ms frames). The engine
transcodes to and from its internal PCM representation, so you publish and
receive in the codec you pass to `attach`. For the full track / namespace model
see
[Sessions, calls, tracks & streams](/modalities/voice/concepts/sessions-calls-tracks-streams).

## Full example: originate, attach, tap

Originate a call, let a server-side agent run the ASR/LLM/TTS loop, and tap the
uplink in parallel to write a transcript-friendly frame log.

```python
import os, signal, struct, threading, time
from clutchcall.voice import Voice

v = Voice(
    base_url="https://engine.firstbyte.fast",
    api_key=os.environ["CLUTCHCALL_API_KEY"],
    org_id="org_abc",
)

# 1. Originate; `agent` lets the engine drive the audio end-to-end.
call = v.calls.originate(
    to="+15551234567", from_="+15558675309",
    trunk_id="trunk_main", agent="healthcare-assistant",
)
print(f"dialing: sid={call.sid}")

# 2. Tap the uplink in parallel — write [u32 len][u64 ts_us] + frame.
out = open("caller.opus", "wb")

def on_uplink(frame: bytes, ts_us: int) -> None:
    out.write(struct.pack("<IQ", len(frame), ts_us))
    out.write(frame)

bridge = v.audio_bridge.attach(call.sid, codec="opus", on_uplink=on_uplink)

# 3. Hold until SIGTERM, then tear down cleanly.
stop = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stop.set())
try:
    while not stop.is_set():
        stop.wait(timeout=2.0)
finally:
    bridge.close()
    call.hangup()
    out.close()
```

> **NOTE:**
> This mirrors the `voice_agent_attach.py` example shipped with the SDK.
>   Because the SDK is blocking, run long-lived taps under a thread or process
>   you control. Drive shutdown from a signal, as above.

## Related

  - **[TypeScript SDK](/modalities/voice/sdk/javascript)** — The primary SDK, including the caller-side audio surface.
  - **[Agent control events](/modalities/voice/sdk/events)** — The human-agent control channel and its event model.
  - **[Calls API](/modalities/voice/api/calls)** — The control-plane call routes the SDK wraps.
  - **[Outbound call cookbook](/modalities/voice/cookbooks/outbound-call)** — An end-to-end outbound walkthrough.
  - **[Stream to your ASR](/modalities/voice/cookbooks/stream-to-asr)** — Tap the uplink into your own speech pipeline.
  - **[Agent runtime](/modalities/voice/runtime/overview)** — What a bound server-side agent does with the audio.
