Documentation Index
Fetch the complete documentation index at: https://docs.coval.dev/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Twilio ConversationRelay lets you connect a Twilio Programmable Voice call to a WebSocket server that handles STT → LLM → TTS in real time. This guide covers how to:- Build an OTel span tree from ConversationRelay events and export it to Coval
- Correlate traces with Coval simulation runs despite Twilio PSTN stripping SIP headers
Walkthrough
The PSTN limitation
When Coval places a simulation call to your agent, it normally passes the simulation output ID as a custom SIP header:X-Coval-Simulation-Id never reaches your application.
Solution: pre_call_webhook_url
Coval supports an alternative correlation mechanism for agents where SIP headers are unavailable. Configurepre_call_webhook_url on your agent and Coval will POST the simulation output ID to your agent before dialing, giving it a chance to stash the ID before the call connects.
The webhook is called once per simulation, immediately before the outbound call is placed. It receives:
from_number is the caller ID Coval will dial from. to_number is your agent’s phone number. Use these to correlate the webhook with the incoming call — especially useful when running multiple agent replicas behind a load balancer.
Your agent queues this ID, then pops it when the next call arrives.
Configure your Twilio phone number
Before Coval can place simulation calls to your agent, your Twilio phone number must be configured to route inbound calls to your agent’s webhook.- Open the Twilio Console and navigate to Phone Numbers → Manage → Active Numbers.
- Click the phone number you want to use for simulations.
- Scroll to the Voice Configuration section.
- Set Configure with to Webhook, TwiML Bin, Function, Studio Flow, Proxy Service.
- Under A call comes in, select Webhook and enter your agent’s webhook URL (e.g.
https://your-agent.fly.dev/webhook). Leave HTTP method as HTTP POST. - Save the configuration.
Coval agent configuration
In the Coval dashboard, open your agent’s settings and set the following in the agent metadata:| Field | Description |
|---|---|
pre_call_webhook_url | The URL Coval will POST to before each simulation call |
pre_call_webhook_headers | Optional headers to include — use this to authenticate Coval’s request to your agent |
Agent implementation
/register-simulation endpoint
Add an endpoint that accepts Coval’s pre-call notification and queues the simulation ID:Reading the simulation ID on call arrival
In your ConversationRelay WebSocket handler, pop the pending ID when the"setup" event arrives:
Exporting traces after the call
When the WebSocket closes, build OTLP spans from your turn log and POST them to Coval:_send_spans in the finally block of your WebSocket handler, after the call ends:
Async audio attachment (multi-replica deployments)
Twilio Programmable Voice recording URLs are not retrievable at call end — Twilio finalizes the recording asynchronously, typically ~60 seconds later. If your agent runs as a single long-lived process you can simply wait, but in a multi-replica Kubernetes or Fly.io deployment the agent container may be terminated before the URL becomes available. For those deployments, split conversation submission across two API calls:- At call end,
POST /v1/conversations:submitwith the transcript only. You immediately get aconversation_idand text-only metrics start running. Flush your buffered OTel spans with thisconversation_idso traces correlate with the conversation. - When the recording URL is ready,
PATCH /v1/conversations/{conversation_id}with theaudio_url. Audio-dependent metrics then run as a second wave.
PATCH /v1/conversations/{conversation_id} returns 409 ALREADY_EXISTS.
Trace limitations
ConversationRelay abstracts STT and TTS away from your application code entirely — you receive transcribed text in"prompt" events and send text tokens back; Twilio handles the rest. This means several span values cannot be measured and must be approximated. These are architectural constraints of the ConversationRelay model, not implementation choices.
| Value | Why it must be synthetic |
|---|---|
stt → metrics.ttfb | Twilio performs speech recognition internally. Your application only receives the final transcribed text in a "prompt" WebSocket event — there is no timestamp for when speech started or when transcription completed. |
stt → stt.confidence | Twilio does not expose per-utterance ASR confidence scores through the ConversationRelay WebSocket API. Fixed at 0.95. |
tts → metrics.ttfb | Twilio converts your text tokens to audio internally. Your application has no visibility into when audio playback begins at the caller’s end. Fixed at 0.1s. |
llm → metrics.ttfb. Because your application makes the LLM API call directly, you can measure wall-clock time from when the "prompt" event arrives to when the first response token is sent back. This is the only latency signal from ConversationRelay traces worth trusting.
Practical implication: Coval’s built-in STT TTFB and TTS TTFB latency metrics will not reflect real performance for Twilio ConversationRelay agents. LLM TTFB metrics will. If you need real STT/TTS timing data, consider a framework where you control the STT and TTS API calls directly (e.g., Pipecat or LiveKit), which expose those timings to your instrumentation code.
Span schema
| Span | Key attributes | Notes |
|---|---|---|
conversation | call.duration_seconds | Root span |
stt | transcript, metrics.ttfb (synthetic), stt.confidence (synthetic 0.95) | One per user turn |
stt.provider.twilio | stt.providerName, stt.confidence, metrics.ttfb | Child of stt |
llm | metrics.ttfb (real), llm.finish_reason | One per assistant turn |
tts | metrics.ttfb (synthetic 0.1s) | One per assistant turn |
llm_tool_call | function.name, tool_call_id, function.arguments | When tools are invoked |
tool_call_result | function.name, tool_call_id, tool.result | Status = ERROR if tool returned an error |
Viewing traces
After a simulation completes, an OTel Traces card appears in the metric grid on the result page. Click View Traces to open the trace viewer.
If no traces appear, check:
pre_call_webhook_urlis set on the Coval agent and points to the correct URL- Your
/register-simulationendpoint is publicly accessible and returning200 OK - The
COVAL_API_KEYin thepre_call_webhook_headersmatches what your agent expects COVAL_API_KEYis set in the agent environment (needed to export spans)
Full example
See the complete working implementation in coval-examples/voice-agents/twilio, which includes:- ConversationRelay WebSocket handler with interrupt support
- Agentic LLM loop (tool calls → re-enter loop until
finish_reason = stop) - Full span builder with real LLM TTFB measurement
- Fly.io deployment configuration

