- Control-plane API —
originate,transfer,hangupand the other control calls run over tRPC to the control-plane API. Failures come back as an HTTP status plus aVoiceErrorin 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
statusplus a raw Q.850 cause on the terminalclearedevent, not as an exception on youroriginatecall. - 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 acleared 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 status | Q.850 cause | Meaning | Typical SIP response |
|---|---|---|---|
completed | 16 (Normal clearing) | Answered with audio, or an immediate normal hangup | 200 OK then BYE |
busy | 17 (User busy) | Callee busy | 486 Busy Here |
no-answer | 18 (No user responding) | Rang, nobody picked up in time | 408 Request Timeout |
no-answer | 19 (No answer — user alerted) | Alerted, then timed out or was cancelled | 480 Temporarily Unavailable / CANCEL |
rejected | 21 (Call rejected) | Callee actively declined | 603 Decline |
failed | any other cause | Everything else — congestion, unallocated number, routing failure | 4xx/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.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).
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 aVoiceError, then surfaces the server’s HTTP status for anything the API
rejects.
- TypeScript
- Python
| 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 it belongs to |
| 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 |
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.
| Symptom | Cause | Handling |
|---|---|---|
Client stuck in Connecting / flips to Failed | Handshake blocked — UDP/443 filtered, wrong/expired dev cert, or a proxy in the path | Verify 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 churning | Network flap or NAT rebind | Usually self-heals; the session re-establishes pubs/subs. Persistent churn ⇒ inspect QUIC transport telemetry |
| Capture throws immediately in the browser | Encoded-transform (RTCRtpScriptTransform) 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 | Namespace scope 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 |
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: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 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 | Remote MCP server returned an error | the server’s error message, re-wrapped |
mcp | Malformed / non-JSON response | parse-error envelope |
client | Runtime asked the connected client to run the tool | client-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.Handling patterns
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.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.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.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.Related
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.