# Rust

> Originate, transfer, and stream call audio from Rust with the synchronous Voice client and the MoQT AudioBridge.

The Rust Voice SDK gives you the same two planes as every other FirstByte
SDK, in idiomatic Rust:

- a **control plane** — `Calls`, `Agents` — a small blocking client over the
  control-plane API (originate, transfer, hangup, bind an agent), and
- a **data plane** — `AudioBridge` — bidirectional call audio (Opus / PCM16 /
  G.711) over QUIC (MoQT), with the `voice/<sid>/{uplink,downlink}` track
  convention enforced for you.

It mirrors the [TypeScript](/modalities/voice/sdk/javascript),
[Python](/modalities/voice/sdk/python), and [Go](/modalities/voice/sdk/go)
Voice modules, with the same route names and the same track layout. A backend
written against any of them ports over method-for-method.

> **NOTE:**
> The control-plane client (`Calls`/`Agents`) is **synchronous and blocking** (it
> uses the control-plane tRPC surface over HTTPS). The `AudioBridge` data plane
> binds a bundled native transport library through callbacks. That library is
> the shared QUIC/MoQT engine core used by every FirstByte native SDK. There
> is no `async`/`tokio` Voice surface today. Run the control-plane calls from a
> blocking context (or `tokio::task::spawn_blocking`). Treat the audio callbacks
> as code that runs on the transport thread.
> See [Native transport library](#native-transport-library).

## Install

The Voice module ships in the FirstByte Rust SDK crate. Add it to your
`Cargo.toml`:

```toml
[dependencies]
clutchcall = "1"
```

The realtime tracks bind a bundled native transport library, so build and run
with it on your linker and library search path:

```bash
RUSTFLAGS="-L /path/to/lib" LD_LIBRARY_PATH=/path/to/lib cargo run
```

## Connect

Construct a `Voice` client with your control-plane API base URL, an API key,
and your workspace (org) id. The key and org scope every control-plane request.
The client also carries the `relay_host` that your audio bridges dial.

```rust
use clutchcall::voice::Voice;

let voice = Voice::new(
    "https://engine.firstbyte.fast",           // control-plane API base URL
    &std::env::var("CLUTCHCALL_API_KEY").unwrap(),
    "org_abc123",                        // your workspace id
)?;

// Audio bridges dial your workspace relay by default; override if needed.
let mut voice = voice;
voice.relay_host = "relay.firstbyte.fast".into();
```

`Voice` hands out the three sub-clients:

| Method | Returns | Plane |
| --- | --- | --- |
| `voice.calls()` | `Calls` | Control — originate / fetch / transfer / hangup |
| `voice.agents()` | `Agents` | Control — bind a server-side agent to a call |
| `voice.audio_bridge()` | `AudioBridgeFactory` | Data — attach to a live call's audio |

> **WARNING:**
> `Voice::new` returns `Err` if the base URL, API key, or org id is empty. Every
> control-plane method returns `Result<_, VoiceError>`. A non-2xx or tRPC error
> response surfaces as `VoiceError(message)`. Match on it. Do not unwrap in
> production.

## Place and control a call

1. **Originate**
`Calls::originate` takes an `OriginateArgs` and returns a `Call` whose `data`
field carries the assigned `sid` and initial status.

```rust
use clutchcall::voice::OriginateArgs;

let call = voice.calls().originate(OriginateArgs {
    to:       "+14155550101",
    from:     "+14155550100",
    trunk_id: "trunk_main",
    agent:    Some("agent_support"), // bind a server-side agent, or None
    ring_timeout_sec: 30,
})?;

println!("call {} is {}", call.data.sid, call.data.status);
```

2. **Fetch an existing call**
Look a call up later by its `sid`:

```rust
let call = voice.calls().get("call_sid_xyz")?;
```

3. **Transfer or hang up**
A `Call` exposes the control operations. Transfers map onto the same
control-plane route (`voice.calls.transfer`). Pass a PSTN number **or** an
agent.

```rust
call.transfer_to("+14155550199")?;   // blind transfer to a PSTN number (REFER)
call.transfer_agent("agent_billing")?; // re-attach to a different agent
call.hangup()?;
```

### `OriginateArgs`

- **`to`** (`&str`, required) — Destination in E.164 (`+14155550101`).
- **`from`** (`&str`, required) — Caller-ID / originating number in E.164.
- **`trunk_id`** (`&str`, required) — The trunk to route the outbound leg over. Configure trunks in the console. See [SIP trunking](/modalities/voice/transport-telephony/sip-trunking).
- **`agent`** (`Option<&str>`, default `None`) — Bind a server-side agent to answer the call. `None` originates a bare call you drive yourself (for example, via `AudioBridge`).
- **`ring_timeout_sec`** (`u32`, default `30`) — The number of seconds to ring before the attempt stops. `OriginateArgs::default()` sets `30`.

- **`Call.data`** (`CallData`) — The returned `Call` carries a `CallData` with `sid`, `status`, `to`, `from`, `started_at`, `trunk_id: Option<String>`, and `agent: Option<String>`.

> **NOTE:**
> `transfer_to` performs a SIP REFER-based transfer. Operator-initiated transfers
> are shipped. Execution of the outbound leg from a **peer-initiated** REFER is
> still partial in the engine. See
> [AI ⇄ human handoff](/modalities/voice/transport-telephony/ai-human-handoff)
> for what is wired end-to-end.

## Stream call audio (`AudioBridge`)

Attach an `AudioBridge` to a live `call_sid` when you want to send and receive
raw media yourself — for example, to feed uplink to your own ASR and play your
own TTS back. In one call, the factory subscribes to the caller's audio
(`voice/<sid>/uplink`) and opens a publication for audio back to the caller
(`voice/<sid>/downlink`).

```rust
use clutchcall::voice::{AudioBridgeOpts, Codec};

let bridge = voice.audio_bridge().attach(
    &call.data.sid,
    AudioBridgeOpts { codec: Codec::Pcm16, ..Default::default() },
    // uplink callback: (frame_bytes, timestamp_us) — caller -> you
    |frame: &[u8], ts_us: u64| {
        // hand `frame` to your ASR / recorder / mixer
    },
)?;

// downlink: your audio -> caller. Timestamps are stamped for you.
bridge.publish_downlink(&pcm16_frame);
```

- **`codec`** (`Codec`, default `Codec::Opus`) — `Codec::Opus | Pcm16 | G711ULaw | G711ALaw`. This is the on-track capability. Voice is audio-only (there are no video codecs). The runtime bus is 8 kHz PCM16 end-to-end — see [Codecs](/modalities/voice/transport-telephony/codecs).
- **`sample_rate`** (`u32`, default `48000`) — Wire sample rate. `AudioBridgeOpts::default()` is Opus / 48000 / 1 channel / 20 ms.
- **`channels`** (`u8`, default `1`) — Channel count (voice is mono).
- **`frame_ms`** (`u16`, default `20`) — Frame duration in milliseconds (20 ms = one media tick).

> **WARNING:**
> `AudioBridge` closes on **drop** (RAII). A drop tears down both the
> subscription and the publication. There is no explicit `close()`. Keep the
> `bridge` alive for as long as you want audio to flow (bind it to a `let`, not
> `let _`). The uplink callback signature is `FnMut(&[u8], u64) + Send + 'static`
> — `(frame, timestamp_us)`.

## Bind a server-side agent

To let a configured agent (ASR/LLM/TTS or a speech-to-speech provider) run the
conversation, attach one to a live call instead of bridging audio yourself:

```rust
voice.agents().attach(&call.data.sid, "agent_support")?;
```

You define the agent in the console. The runtime picks up its pipeline config
per call. See the [Agent Runtime overview](/modalities/voice/runtime/overview).

## End-to-end example

```rust
use clutchcall::voice::{Voice, OriginateArgs, AudioBridgeOpts, Codec};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut voice = Voice::new(
        "https://engine.firstbyte.fast",
        &std::env::var("CLUTCHCALL_API_KEY")?,
        "org_abc123",
    )?;
    voice.relay_host = "relay.firstbyte.fast".into();

    // 1. Place an outbound call (no server agent — we'll drive audio).
    let call = voice.calls().originate(OriginateArgs {
        to:   "+14155550101",
        from: "+14155550100",
        trunk_id: "trunk_main",
        agent: None,
        ring_timeout_sec: 30,
    })?;
    println!("dialing {} -> {}", call.data.from, call.data.to);

    // 2. Attach an audio bridge; forward uplink to your own pipeline.
    let bridge = voice.audio_bridge().attach(
        &call.data.sid,
        AudioBridgeOpts { codec: Codec::Pcm16, ..Default::default() },
        |frame, _ts_us| { /* feed your ASR here */ let _ = frame; },
    )?;

    // 3. ... run your loop, call bridge.publish_downlink(&frame) to talk back ...
    let _ = &bridge;

    // 4. Hang up (dropping `bridge` closes the media legs).
    call.hangup()?;
    Ok(())
}
```

## Native transport library

The control plane (`Calls`, `Agents`) is pure Rust over HTTPS and needs no
native dependency. The **data plane** (`AudioBridge`, and the underlying
`MoqtClient` in the crate's `moqt` module) binds the bundled native QUIC/MoQT
transport core, the same engine core shared across the native SDKs. This
means:

- Build and run with the native library on your linker/library path
  (`RUSTFLAGS="-L …"`, `LD_LIBRARY_PATH=…`).
- The transport **auto-reconnects** with backoff. It re-announces and
  re-subscribes every track on reconnect, with no app code. You can publish
  and subscribe before the session is up. The SDK queues those calls and
  replays them on connect.
- Native SDKs get the **QUIC → WebSocket fallback ladder** transparently for
  UDP-blocked networks. (The browser/TS SDK is WebTransport-only today; this
  fallback is a native-core benefit.)

> **NOTE:**
> The lower-level track API is available directly as `clutchcall::moqt`
> (`MoqtClient::connect`, `publish_audio`, `subscribe_audio`). Use it if you
> need capabilities beyond the Voice convention — for example, a subscription
> with an explicit filter or window. `AudioBridge` is the wrapper that applies
> the `voice/<sid>/{uplink,downlink}` naming for you.

## What's not in the Rust surface

Know these limits before you look for a feature that is not here:

- **No async Voice API.** Control-plane calls block. There is no `tokio`-native
  Voice client. Wrap the calls in `spawn_blocking` if you are inside a runtime.
- **No human-agent control channel.** The event-driven softphone/agent control
  channel (call-offered / accept / reject) currently ships only in the
  TypeScript SDK. In Rust, drive calls via `Calls`/`Agents` and bridge audio via
  `AudioBridge`.
- **No browser capture.** WebRTC-diversion mic capture is a browser-only concern
  ([Browser audio capture](/modalities/voice/transport-web/browser-audio-capture)).
  The Rust SDK is a **server-side / backend** client.

## Next steps

  - **[Calls API](/modalities/voice/api/calls)** — The control-plane routes behind `Calls` — originate, transfer, hangup.
  - **[Transport & tracks](/modalities/voice/api/transport)** — The MoQT track model `AudioBridge` builds on.
  - **[Sessions, calls & tracks](/modalities/voice/concepts/sessions-calls-tracks-streams)** — How `call_sid` and the `voice/<sid>/{uplink,downlink}` layout fit together.
  - **[Outbound call cookbook](/modalities/voice/cookbooks/outbound-call)** — A full originate → stream → hang up walkthrough.
