# Building Voice Agents

> Build a production voice agent end to end: place or answer a call, attach an agent, tune turns, add tools, hand off to a human, and record.

This guide binds the whole voice stack together. At the end, you will have a
production voice agent. It dials out (or answers). It reasons over the caller
with a model. It calls your tools mid-conversation. It hands off to a human
when needed. It records both legs for review.

Every other page in this section goes deep on one piece:
[turn detection](/modalities/voice/concepts/turn-detection),
[tool calling](/modalities/voice/runtime/tool-calling),
[handoffs](/modalities/voice/concepts/handoffs). This page walks through the
assembly in order. It links out at each step. It shows the one control-plane
call that starts it all.

> **NOTE:**
> You use one identifier throughout: the **`sid`** (call_sid). The engine mints
>   it once — at the inbound `INVITE` or when you call `originate()`. After that,
>   the `sid` addresses everything: audio, transfer, hangup, recording,
>   telemetry. You never mint a second one.

## What you'll assemble

  - **A call** — Originate outbound over a SIP trunk. Or answer an inbound call that your
    dialplan routes.
  - **An agent** — A server-side runtime that transcribes, reasons, speaks, and stops when
    interrupted. You attach it to the call by id.
  - **A safety net** — Tools, a warm human handoff, and both-leg recording. A production agent
    cannot ship without these.

## Before you start

  1. **Provision a workspace**
Choose managed cloud or self-hosted. Both give you a tenant with an API key
    and a per-tenant SIP subdomain. See
    [managed cloud](/modalities/voice/quickstart/managed-cloud) or
    [on-prem](/modalities/voice/quickstart/on-prem).
  2. **Configure a SIP trunk**
A trunk carries calls to and from a carrier. Note its `trunkId`. See
    [SIP trunking](/modalities/voice/transport-telephony/sip-trunking).
  3. **Define an agent**
An agent is a runtime config: a pipeline plus turn detection and tools.
    Author it in the [Voice AI console](https://voiceai.firstbyte.fast) or by
    config. Note its `agent` id. The shape is covered in
    [runtime overview](/modalities/voice/runtime/overview).
  4. **Initialize the Voice client**
**TypeScript:**
```ts
    import { Voice } from "clutchcall_connect/voice";

    const v = new Voice({
      baseUrl: "https://engine.firstbyte.fast",
      apiKey:  process.env.CLUTCHCALL_CREDENTIALS!,
      orgId:   "org_abc",
    });
    ```
    **Python:**
```python
    import os
    from clutchcall.voice import Voice

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

## Step 1 — Get a call

You either **start** a call (outbound) or **answer** one that arrives
(inbound). Both end with a live `sid`. You can attach an agent to that `sid`.

  
    This is one control-plane call. Pass the agent id, and the engine attaches
    the agent automatically at the moment the callee answers. Then you can skip
    Step 2 entirely.

    
    **TypeScript:**
```ts
    const call = await v.calls.originate({
      to:      "+15551234567",   // E.164 destination
      from:    "+15558675309",   // caller-id you're allowed to present
      trunkId: "trunk_main",
      agent:   "agent_sales",    // attach on answer
    });

    console.log(call.sid, call.status); // "call_9f2c…" "dialing"
    ```
    **Python:**
```python
    call = v.calls.originate(
        to="+15551234567",
        from_="+15558675309",
        trunk_id="trunk_main",
        agent="agent_sales",
    )
    print(call.sid, call.status)  # "call_9f2c…" "dialing"
    ```
    

    > **WARNING:**
> `originate` is control-plane. It returns at `dialing`, **not** when the
>       callee answers. Read the outcome with `calls.get(sid)` or from the
>       [call-lifecycle events](/modalities/voice/transport-telephony/call-lifecycle).
>       The full walkthrough is in
>       [outbound calls](/modalities/voice/transport-telephony/outbound).
  
  
    Inbound calls do not originate from your code. The carrier sends an
    `INVITE`. The SIP gateway answers. Your **dialplan** routes the call. To
    send the caller straight to an agent, point the DID's inbound rule at
    `ai_bidirectional_stream <agent_id>`. The engine then answers with early
    media and attaches the agent for you. There is no SDK call on the answer
    path.

    ```
    carrier trunk ──INVITE──▶ SIP gateway ──▶ dialplan route
        └▶ "ai_bidirectional_stream agent_support" ──▶ agent attached, media flowing
    ```

    You configure that routing in the console or in the trunk config. See
    [inbound calls](/modalities/voice/transport-telephony/inbound) and
    [PBX & ACD](/modalities/voice/transport-telephony/pbx-acd) for menus and
    skill queues. To watch or act on the live call from your backend, subscribe
    to [lifecycle events](/modalities/voice/transport-telephony/call-lifecycle)
    keyed on the `sid`.
  

## Step 2 — Attach the agent

Maybe you did not pass `agent` to `originate`. Or you answer inbound from code
and want to bind an agent yourself. In both cases, attach an agent to the live
`sid`. The engine wires the audio bridge end to end: caller audio into the
agent, and agent audio back to the caller. You do not open a bridge yourself.

**TypeScript:**
```ts
await v.agents.attach(call.sid, "agent_support");
```
**Python:**
```python
v.agents.attach(call.sid, "agent_support")
```

> **NOTE:**
> An attach binds a **server-side** agent. The agent runs inside the voice
>   gateway, on the core that owns the call's media. Thus turn detection and
>   barge-in are wired straight into the media path. You may instead want to run
>   your own runtime and drive raw audio frames yourself. In that case, open an
>   [`AudioBridge`](/modalities/voice/sdk/javascript) and feed it. Or bring an
>   external runtime via
>   [keep your existing runtime](/modalities/voice/runtime/keep-existing).

## Step 3 — Configure turn detection

Turn detection makes the call feel human. The agent stops the instant the
caller talks (barge-in). The agent replies the instant the caller finishes.
Turn detection lives in the agent config, not in your call code. Set it once
per agent.

Pick a mode. Cascaded pipelines use on-device VAD. Realtime models let the
provider own turns.

```yaml
# agent config — turn detection block
entry_node: ASR
turn_detection:
  type: silero            # on-device VAD for cascaded pipelines (default)
  silence_threshold_ms: 500   # trailing silence before end-of-utterance fires
  min_speech_ms: 300          # discard clicks / line noise shorter than this
  prefix_padding_ms: 200      # audio kept before the speech-start marker
nodes:
  ASR: { provider: deepgram }
  LLM: { provider: anthropic }
  TTS: { provider: elevenlabs }
```

- **`type`** (`"silero" | "server_vad"`, default `"silero"`) — `silero` runs on-device VAD in the gateway. It is the default for cascaded   ASR → LLM → TTS. `server_vad` lets a realtime provider own turn signalling.   A `REALTIME` entry node auto-promotes to `server_vad` unless you set `type`   explicitly.
- **`silence_threshold_ms`** (`number`, default `500`) — Trailing silence before end-of-utterance fires. Lower it (~300) for faster   replies. Raise it (~800) for callers who pause to think.
- **`min_speech_ms`** (`number`, default `300`) — Minimum speech duration to count as a real utterance. Raise it if line noise   causes false triggers.

> **TIP:**
> Backchannels ("mhm", "yeah") must not cut the agent off. The runtime uses
>   **hold-and-confirm** barge-in for exactly this reason. A short interjection
>   never cancels the agent. This is on by default. The full model, and the
>   PSTN-vs-browser echo-gating knobs, are in
>   [turn detection](/modalities/voice/concepts/turn-detection).

## Step 4 — Add tools

A useful agent does things: it looks up an order, books a slot, checks a
balance. Declare tools in the agent config. The model calls them
mid-conversation. The runtime feeds results back and keeps the caller engaged.

```json
{
  "kind": "http",
  "name": "lookup_order",
  "description": "Look up an order by its ID and return its status.",
  "spec": {
    "method": "GET",
    "url": "https://api.example.com/orders/{{order_id}}",
    "auth": { "type": "bearer", "token": "sk_live_..." },
    "parameters": {
      "type": "object",
      "properties": {
        "order_id": { "type": "string", "description": "Order id to look up." }
      },
      "required": ["order_id"]
    },
    "timeout_ms": 10000
  }
}
```

Three tool kinds ship today: **`http`** (call an endpoint that you own),
**`mcp`** (a remote [MCP](https://modelcontextprotocol.io) server over
Streamable HTTP), and **telephony** tools such as a route to a live human
queue. The full parameter reference, MCP setup, and per-node allow-lists are in
[tool calling](/modalities/voice/runtime/tool-calling).

## Step 5 — Hand off to a human

The agent thesis for regulated work: the model qualifies, authenticates, and
captures compliance context. Then it escalates to a person. This is a **warm**
handoff, not a cold transfer. The caller keeps their `sid` and hears
uninterrupted audio. Only the agent leg re-points.

There are two shapes. Pick by target.

  - **Re-attach to another agent** — Swap the AI agent for a different one — same call, new brain.

    ```ts
    await call.transfer({ agent: "agent_tier2" });
    ```
  - **Escalate to a human** — Route to a live human queue (browser softphone or SIP deskphone). The
    telephony tool `route_to_skill` — or an operator `transfer` — rings the
    next available agent and bridges them onto the caller leg.

**TypeScript:**
```ts
// forward the whole call to a PSTN number …
await call.transfer("+15550000000");
// … or re-attach a different agent to the same sid
await call.transfer({ agent: "agent_tier2" });
```
**Python:**
```python
call.transfer("+15550000000")
call.transfer(agent="agent_tier2")
```

> **NOTE:**
> On the wire, escalation pauses the AI leg. It mirrors the caller's audio to
>   the human. It injects the human's mic back to the caller. The caller never
>   hears a gap. The ACD drives the queue routing and the human-agent ring
>   (browser or deskphone). The mechanics are in
>   [AI ↔ human handoff](/modalities/voice/transport-telephony/ai-human-handoff).
>   The product-level bot ⇄ human framing (and warm-handoff context) is in
>   [handoffs](/modalities/voice/concepts/handoffs).

## Step 6 — Record and review

Recording captures **both legs**: the caller on the left channel, the agent on
the right. It lands a stereo WAV in your recording store for playback and
review. The engine scores media QoS (loss, jitter, MOS) per call at the same
time.

> **NOTE:**
> Recording is a **workspace/trunk-level** capability, not a per-call SDK flag
>   in the current surface. You enable it on the trunk (or workspace). Then the
>   system captures every call that the trunk carries. There is no
>   `record: true` argument on `originate()` today. To analyze recordings
>   programmatically, pull them and the QoS metrics from the observability
>   plane.

When recording is on, wire up review:

  - **[Record & analyze](/modalities/voice/cookbooks/record-analyze)** — Pull both legs and run analysis over the stereo capture.
  - **[MOS, jitter & loss](/modalities/voice/observability/mos-jitter-loss)** — The per-call media-quality score and what drives it.
  - **[Call traces](/modalities/voice/observability/call-traces)** — The event-level trace of a single call, keyed on its `sid`.
  - **[Dashboards](/modalities/voice/observability/dashboards)** — Fleet-wide call, quality, and agent reports.

## Step 7 — Tear down

End the call cleanly. Hangup releases the media legs. It stops the agent
session. It sends the final lifecycle event and the CDR.

**TypeScript:**
```ts
await call.hangup();
```
**Python:**
```python
call.hangup()
```

> **TIP:**
> You rarely need to poll for the end. The runtime's **idle watchdog**
>   re-engages a silent caller ("still there?"). It drops the AI leg if the
>   caller is gone. There is also a hard max-session ceiling. Tune both in
>   [session lifecycle](/modalities/voice/runtime/session-lifecycle).

## Put it together

This is the whole outbound-to-teardown flow, minus the config that lives in the
agent definition:

**TypeScript:**
```ts
import { Voice } from "clutchcall_connect/voice";

const v = new Voice({
  baseUrl: "https://engine.firstbyte.fast",
  apiKey:  process.env.CLUTCHCALL_CREDENTIALS!,
  orgId:   "org_abc",
});

// 1. originate + attach the agent on answer
const call = await v.calls.originate({
  to: "+15551234567", from: "+15558675309",
  trunkId: "trunk_main", agent: "agent_sales",
});

// 2. …the agent runs the conversation, calls tools, and can escalate…
//    (turn detection + tools live in the agent config; recording on the trunk)
await call.transfer({ agent: "agent_tier2" }); // e.g. mid-call re-attach

// 3. tear down
await call.hangup();
```
**Python:**
```python
import os
from clutchcall.voice import Voice

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

call = v.calls.originate(
    to="+15551234567", from_="+15558675309",
    trunk_id="trunk_main", agent="agent_sales",
)

call.transfer(agent="agent_tier2")
call.hangup()
```

## Next steps

  - **[Inbound call recipe](/modalities/voice/cookbooks/inbound-call)** — The answer-and-route path, end to end.
  - **[Outbound call recipe](/modalities/voice/cookbooks/outbound-call)** — Originate, poll, and read the outcome.
  - **[Transfer to a human](/modalities/voice/cookbooks/transfer-human)** — A runnable warm-handoff recipe.
  - **[Bring your own runtime](/modalities/voice/runtime/keep-existing)** — Use FirstByte as pure transport with an external agent runtime.
