Voice failures surface on three different planes, and knowing which one you are looking at tells you where to fix it:
  • Control-plane APIoriginate, 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 shows up after the fact on the CDR as a coarse status plus a raw Q.850 cause on the terminal cleared event, not as an exception on your originate call.
  • Transport & tools — the media/data plane (WebTransport/MoQT) and any agent tool calls fail independently of the control plane and are reported through connection state and structured tool-error envelopes.
originate returning success means the engine accepted the request and started dialing — it does not mean the far end answered. Watch the call lifecycle events (initiated → delivered → established → cleared) or the CDR status to learn the outcome. See Call lifecycle.

Call-failure causes

Every call ends with a cleared lifecycle event carrying a Q.850 cause and a talk duration. The CDR pipeline folds those into a coarse status that the reports and CDR views read. The rule is simple: any call with talk time > 0 is completed; a zero-duration call is labeled by its cause.
CDR statusQ.850 causeMeaningTypical SIP response
completed16 (Normal clearing)Answered with audio, or an immediate normal hangup200 OK then BYE
busy17 (User busy)Callee busy486 Busy Here
no-answer18 (No user responding)Rang, nobody picked up in time408 Request Timeout
no-answer19 (No answer — user alerted)Alerted, then timed out or was cancelled480 Temporarily Unavailable / CANCEL
rejected21 (Call rejected)Callee actively declined603 Decline
failedany other causeEverything else — congestion, unallocated number, routing failure4xx/5xx/6xx
status is derived by the CDR builder: duration_sec > 0 always wins and marks the call completed, even for cause 16 with a very short talk time. Only zero-duration calls are classified by their Q.850 cause; unmapped causes fall through to failed. The raw q850_cause is preserved on the CDR (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, trunk, talk duration, and — when the call had engine-anchored media — per-call QoS (packet loss, jitter, and an E-model MOS).
A recurring cause of 488-flavored failed calls is a codec/SDP mismatch on the trunk (for example a single-codec answer the carrier rejects). These fail at INVITE time, never reach established, and clear with a non-standard cause. See Call not connecting.

Auth errors

Control-plane calls authenticate with your API key and workspace (org) id. The SDK validates required inputs before it ever hits the network and throws a VoiceError, then surfaces the server’s HTTP status for anything the API rejects.
import { Voice, VoiceError } from "@telequick/sdk";

try {
  const voice = new Voice({
    baseUrl: "https://portal.telequick.dev",
    apiKey: process.env.TELEQUICK_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;
}
ConditionWhereWhat you seeFix
Missing baseUrl / apiKey / orgIdSDK, before the requestVoiceError("apiKey required")Supply the client option / env var
Bad, expired, or revoked API keyControl-plane APIVoiceError("tRPC <path> 401: …")Rotate the key; check the workspace it belongs to
Key valid but not entitled to the resourceControl-plane APIVoiceError("tRPC <path> 403: …")Grant the role/entitlement in the console
Malformed arguments (e.g. transfer with neither to nor agent)SDKVoiceError("transfer: provide either to:<E.164> or agent:<id>")Fix the call arguments
SIP-side auth is a separate surface. Trunk and softphone REGISTER/INVITE run through the SIP gateway’s internal registrar with multidomain digest auth and a source-IP ACL. A rejected credential returns 401/403 on the SIP dialog (not a VoiceError); an INVITE from an un-allowlisted source is answered 403 Forbidden; and when STIR/SHAKEN policy is set to reject, an INVITE with an invalid attestation is answered 438 Invalid Identity Header. A common trunk-registration gotcha: sealed credentials must be un-sealed before the digest is computed, or the carrier reports “Digest auth failed.” See SIP trunking and SIP trunk troubleshooting.

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 and replays your publishes/subscribes, so treat a transient Reconnecting as normal.
SymptomCauseHandling
Client stuck in Connecting / flips to FailedHandshake blocked — UDP/443 filtered, wrong/expired dev cert, or a proxy in the pathVerify reachability to relay.telequick.dev; on localhost use a fresh ECDSA cert (≤14 days) and dial 127.0.0.1 (IPv6 ::1 breaks the dev handshake)
Reconnecting churningNetwork flap or NAT rebindUsually self-heals; the session re-establishes pubs/subs. Persistent churn ⇒ inspect QUIC transport telemetry
Capture throws immediately in the browserEncoded-transform (RTCRtpScriptTransform) unavailableHard 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 connectNamespace scope or a session-setup raceMoQT 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
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 — there is no automatic WS fallback in the browser, and it deliberately throws rather than silently degrading when encoded transform is missing. This is a known gap, not a misconfiguration. See Browser audio troubleshooting.
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.

Tool errors

When an agent calls a tool, failures are not thrown — the runtime returns a structured error envelope back to the model as the tool result, so the agent can say “I can’t do that right now” or fall back to another tool. Every tool error has the same shape:
{ "error": "http tool 'lookup_order' timed out after 8000ms" }
error
string
Human-readable failure reason. Present only on failure; a successful tool returns its raw result body (an empty result is normalized to {}).
Tool type (type)Error conditionEnvelope message
httpBad spec at loadhttp_tool spec error: …
httpInvalid arguments JSON from the modelparse-error envelope
httpEndpoint timeouthttp tool '<name>' timed out after <ms>ms
httpConnection / transport failurehttp tool '<name>': <reason>
mcpRemote MCP server returned an errorthe server’s error message, re-wrapped
mcpMalformed / non-JSON responseparse-error envelope
clientRuntime asked the connected client to run the toolclient-side tool '<name>' not yet plumbed …
HTTP and MCP tool timeouts are bounded: the timeout_ms you set is clamped to [100ms, 60000ms]. A tool that hangs returns the timeout envelope rather than stalling the turn.
Client-side tools are not yet plumbed. A tool declared with type: "client" (the runtime asking the connected SDK client to execute) currently returns a not yet plumbed error envelope every time it is invoked — the client-tool wire channel has not shipped. Client tools are preview — use http or mcp tools for anything you need working today. HTTP tools, MCP tools, and the telephony transfer tool are shipped. See Agent tools.

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 from the absence of an exception.
2

Branch on the coarse status, drill on Q.850

Route your retry logic on status (busy/no-answer are often retryable; rejected usually is not), and log the raw q850_cause so a bucket of failed calls can be split by their real cause later.
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; only surface an error to your user after a sustained Failed, and remember the browser SDK will 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 spot a flaky endpoint or a client tool that is still preview.

Call lifecycle

The initiated → delivered → established → cleared event flow that carries the Q.850 cause.

Call not connecting

Diagnose INVITE-time failures, codec/SDP mismatches, and trunk rejects.

SIP & RTP debugging

Pull SIP traces and per-call QoS to see the real cause behind a failed call.

Agent tools

How HTTP, MCP, client, and telephony tools are declared and invoked.