# Errors

> Voice error reference: call-failure causes, Q.850 mapping, auth, transport, and tool errors, with handling patterns.

Voice failures show on three different planes. The plane tells you where to fix
the failure:

- **Control-plane API** — `originate`, `transfer`, `hangup` and the other
  control calls run over tRPC to the control-plane API. Failures come back as an
  HTTP status plus a `VoiceError` in the SDK. Fix these in your integration code
  or credentials.
- **The call itself** — a call that reaches the carrier can still end
  unanswered, busy, or rejected. This outcome shows **after the fact** on the
  CDR as a coarse `status` plus a raw **Q.850 cause** on the terminal `cleared`
  event. It does not show as an exception on your `originate` call.
- **Transport &amp; tools** — the media/data plane (WebTransport/MoQT) and any
  agent tool calls fail independently of the control plane. Connection state
  reports transport failures. Structured tool-error envelopes report tool
  failures.

> **NOTE:**
> When `originate` returns success, the engine **accepted** the request and
> started to dial. Success does **not** mean that the far end answered. Watch the
> call lifecycle events (`initiated → delivered → established → cleared`) or the
> CDR `status` to learn the outcome. See
> [Call lifecycle](/modalities/voice/transport-telephony/call-lifecycle).

## Call-failure causes

Every call ends with a `cleared` lifecycle event. The event carries a **Q.850
cause** and a talk duration. The CDR pipeline folds these into a coarse
`status`. The reports and CDR views read this `status`. The rule is simple:
**any call with talk time &gt; 0 is `completed`**. The cause labels a
zero-duration call.

| CDR `status` | Q.850 cause | Meaning | Typical SIP response |
|---|---|---|---|
| `completed` | 16 (Normal clearing) | The call was answered with audio, or ended with an immediate normal hangup | `200 OK` then `BYE` |
| `busy` | 17 (User busy) | The callee was busy | `486 Busy Here` |
| `no-answer` | 18 (No user responding) | The call rang, but nobody answered in time | `408 Request Timeout` |
| `no-answer` | 19 (No answer — user alerted) | The callee was alerted, then the call timed out or was cancelled | `480 Temporarily Unavailable` / `CANCEL` |
| `rejected` | 21 (Call rejected) | The callee actively declined | `603 Decline` |
| `failed` | any other cause | Everything else — congestion, an unallocated number, a routing failure | `4xx`/`5xx`/`6xx` |

> **NOTE:**
> The CDR builder derives `status`. `duration_sec > 0` always wins and marks the
> call `completed`, even for cause 16 with a very short talk time. The Q.850
> cause classifies only zero-duration calls. Unmapped causes fall through to
> `failed`. The CDR keeps the raw `q850_cause` (`0` when unknown), so you can see
> the exact carrier cause behind a coarse `failed`.

The CDR is the durable record of the outcome. For each `cleared` call you get
the coarse `status`, the raw `q850_cause`, the direction, ANI/DNIS, the trunk,
and the talk duration. When the call had engine-anchored media, you also get
per-call QoS: packet loss, jitter, and an
[E-model MOS](/modalities/voice/observability/mos-jitter-loss).

> **WARNING:**
> A codec/SDP mismatch on the trunk is a recurring cause of `488`-flavored
> `failed` calls (for example, a single-codec answer that the carrier rejects).
> These calls fail at INVITE time. They never reach `established`. They clear
> with a non-standard cause. See
> [Call not connecting](/modalities/voice/troubleshooting/call-not-connecting).

## Auth errors

Control-plane calls authenticate with your API key and workspace (org) id. The
SDK validates required inputs before it sends any request. If an input is
missing, the SDK throws a `VoiceError`. If the API rejects a request, the SDK
shows the server's HTTP status in the same `VoiceError`.

```ts
import { Voice, VoiceError } from "@clutchcall/agents";

try {
  const voice = new Voice({
    baseUrl: "https://portal.firstbyte.fast",
    apiKey: process.env.CLUTCHCALL_API_KEY!, // throws VoiceError("apiKey required") if unset
    orgId: "org_123",                          // throws VoiceError("orgId required") if unset
  });

  const call = await voice.calls.originate({ to: "+15551234567", from: "+15557654321" });
} catch (err) {
  if (err instanceof VoiceError) {
    // Client-side validation ("baseUrl required") OR a server error, e.g.
    // "tRPC telephony.originate 401: <body>" for a bad/expired key.
    console.error("voice call rejected:", err.message);
  }
  throw err;
}
```

```python
from clutchcall.voice import Voice, VoiceError

try:
    voice = Voice(
        base_url="https://portal.firstbyte.fast",
        api_key=os.environ["CLUTCHCALL_API_KEY"],
        org_id="org_123",
    )
    call = voice.calls.originate(to="+15551234567", **{"from": "+15557654321"})
except VoiceError as err:
    # Same failure surface: "apiKey required" locally, or the server HTTP status.
    print("voice call rejected:", err)
    raise
```

| Condition | Where | What you see | Fix |
|---|---|---|---|
| Missing `baseUrl` / `apiKey` / `orgId` | SDK, before the request | `VoiceError("apiKey required")` | Supply the client option / env var |
| Bad, expired, or revoked API key | Control-plane API | `VoiceError("tRPC <path> 401: …")` | Rotate the key; check the workspace that owns the key |
| Key valid but not entitled to the resource | Control-plane API | `VoiceError("tRPC <path> 403: …")` | Grant the role/entitlement in the console |
| Malformed arguments (e.g. `transfer` with neither `to` nor `agent`) | SDK | `VoiceError("transfer: provide either to:<E.164> or agent:<id>")` | Fix the call arguments |

> **NOTE:**
> **SIP-side auth** is a separate surface. Trunk and softphone REGISTER/INVITE
> run through the SIP gateway's internal registrar. The registrar uses
> multidomain digest auth and a source-IP ACL. A rejected credential returns
> `401`/`403` on the SIP dialog, not a `VoiceError`. An INVITE from a source that
> is not on the allowlist gets a `403 Forbidden` answer. When STIR/SHAKEN policy
> is set to `reject`, an INVITE with an invalid attestation gets a
> `438 Invalid Identity Header` answer. A common trunk-registration gotcha:
> un-seal sealed credentials before the digest computation, or the carrier
> reports "Digest auth failed." See
> [SIP trunking](/modalities/voice/transport-telephony/sip-trunking) and
> [SIP trunk troubleshooting](/modalities/voice/troubleshooting/sip-trunk).

## Transport errors

The media/data plane is QUIC/HTTP-3 with MoQT over WebTransport on a single
`:443`. Transport failures are **connection-state transitions**, not thrown
exceptions. The client auto-reconnects with capped backoff. The client also
replays your publishes and subscribes. Treat a transient `Reconnecting` as
normal.

| Symptom | Cause | Handling |
|---|---|---|
| Client stuck in `Connecting` / flips to `Failed` | The handshake is blocked — UDP/443 is filtered, the dev cert is wrong or expired, or a proxy is in the path | Check reachability to `relay.firstbyte.fast`. On localhost, use a fresh ECDSA cert (≤14 days) and dial `127.0.0.1` (IPv6 `::1` breaks the dev handshake) |
| `Reconnecting` churning | A network flap or NAT rebind | This usually self-heals; the session re-establishes pubs/subs. For persistent churn, inspect [QUIC transport telemetry](/modalities/voice/observability/dashboards) |
| Capture throws immediately in the browser | Encoded-transform (`RTCRtpScriptTransform`) is unavailable | Hard gate — the browser SDK **refuses to capture** rather than fall back. Use a supported browser (Chrome/Edge 110+, Firefox 133+, Safari TP) |
| Subscribe closes right after connect | A namespace scope problem or a session-setup race | MoQT namespaces must be room-scoped (a 0-element tuple is rejected). Retry; a known browser-mesh setup race can deliver 0 objects on the first attempt |

> **WARNING:**
> The **QUIC→WebSocket fallback ladder ships only in the native SDK core**
> (Python, Go, Rust, Java, .NET). The browser/TypeScript SDK is
> **WebTransport-only today**. The browser has no automatic WS fallback. The
> browser SDK deliberately throws when encoded transform is missing; it does not
> silently degrade. This is a known gap, not a misconfiguration. See
> [Browser audio troubleshooting](/modalities/voice/troubleshooting/browser-audio).

For one-way or missing audio (a call that connected but has no media), the
fault is usually media-plane, not control-plane. See
[One-way audio](/modalities/voice/troubleshooting/one-way-audio).

## Tool errors

When an agent calls a tool, the runtime does **not** throw failures. The
runtime returns a structured error envelope to the model as the tool result.
The agent can then say "I can't do that right now" or fall back to another
tool. Every tool error has the same shape:

```json
{ "error": "http tool 'lookup_order' timed out after 8000ms" }
```

- **`error`** (`string`) — The human-readable failure reason. This field is present **only** on failure. A successful tool returns its raw result body (an empty result is normalized to `{}`).

| Tool type (`type`) | Error condition | Envelope message |
|---|---|---|
| `http` | Bad spec at load | `http_tool spec error: …` |
| `http` | Invalid arguments JSON from the model | parse-error envelope |
| `http` | Endpoint timeout | `http tool '<name>' timed out after <ms>ms` |
| `http` | Connection / transport failure | `http tool '<name>': <reason>` |
| `mcp` | The remote MCP server returned an error | the server's error message, re-wrapped |
| `mcp` | A malformed / non-JSON response | parse-error envelope |
| `client` | The runtime asked the connected client to run the tool | `client-side tool '<name>' not yet plumbed …` |

> **NOTE:**
> **HTTP and MCP tool timeouts** are bounded: the runtime clamps the `timeout_ms`
> that you set to `[100ms, 60000ms]`. A tool that hangs returns the timeout
> envelope. The tool does not stall the turn.

> **WARNING:**
> **Client-side tools are not yet plumbed.** With a tool declared as
> `type: "client"`, the runtime asks the connected SDK client to execute the
> tool. This currently returns a `not yet plumbed` error envelope on every
> invocation. The client-tool wire channel has not shipped. Client tools are
> **preview**. Use `http` or `mcp` tools for anything that you need to work
> today. HTTP tools, MCP tools, and the telephony transfer tool are shipped. See
> [Agent tools](/modalities/voice/runtime/tool-calling).

## Handling patterns

1. **Separate acceptance from outcome**
Treat a successful `originate` as "dialing started." Decide success from the
lifecycle events or the CDR `status`. Never decide success from the absence of
an exception.
2. **Branch on the coarse status, drill on Q.850**
Route your retry logic on `status`. `busy` and `no-answer` are often retryable;
`rejected` usually is not. Log the raw `q850_cause`, so you can later split a
bucket of `failed` calls by their real cause.
3. **Catch VoiceError at the control-plane boundary**
Wrap control calls and inspect `err.message`. The SDK folds both client-side
validation and the server HTTP status into one `VoiceError`. A `401`/`403` in
the message is an auth/entitlement problem, not a call problem.
4. **Treat transport state as advisory**
Let the client auto-reconnect. Show an error to your user only after a
sustained `Failed`. Remember that the browser SDK does not fall back to
WebSocket.
5. **Feed tool errors back to the model**
The runtime already returns tool failures as `{ "error": … }` to the agent. You
generally do not need to intercept them. Monitor them in the agent traces to
find a flaky endpoint or a client tool that is still preview.

## Related

- **[Call lifecycle](/modalities/voice/transport-telephony/call-lifecycle)** — The `initiated → delivered → established → cleared` event flow that carries the Q.850 cause.
- **[Call not connecting](/modalities/voice/troubleshooting/call-not-connecting)** — Diagnose INVITE-time failures, codec/SDP mismatches, and trunk rejects.
- **[SIP &amp; RTP debugging](/modalities/voice/observability/sip-rtp-debugging)** — Pull SIP traces and per-call QoS to see the real cause behind a failed call.
- **[Agent tools](/modalities/voice/runtime/tool-calling)** — How you declare and invoke HTTP, MCP, client, and telephony tools.
