You have a Twilio Voice app: TwiML webhooks, a Media Streams WebSocket, and REST calls. You want it on ClutchCall. The seam is the external agent. Your code runs as a worker. The worker dials one outbound QUIC/WebTransport session to the engine. It authenticates with a media app key/secret. It registers under an agent handle. The engine routes calls to the worker through a VENDOR_BRIDGE agent config. It hands each call to your process as a Call object. Audio flows in both directions as 20 ms pcm16 frames at 8 kHz. The biggest practical difference is the connection direction. Twilio’s cloud calls into your server. Thus you need a public URL, and usually ngrok during development. Here the worker dials out. There are no inbound ports, no public URL, and no tunnel.

What maps to what

Port the speech-assistant sample

The runnable starter is a port of Twilio’s speech-assistant-openai-realtime-api-python sample. The OpenAI Realtime bridge is the upstream file’s code, near-verbatim. That bridge covers session setup, μ-law audio in both directions, and barge-in via conversation.item.truncate. The FastAPI app, TwiML route, and WebSocket plumbing are gone. The starter ships in the provider starters repo next to the telequick-agents SDK.
1

Install the SDK

2

Replace the WebSocket handler with a call handler

The Twilio sample handles "start" / "media" / "mark" JSON events on a WebSocket. The port is a plain coroutine for each call:
main.py
You can send outbound audio in any chunk size. The SDK paces it onto the wire as steady 20 ms frames. Thus bursty Realtime deltas need no re-chunking on your side.
3

Create the external agent and route a number

The console’s External agents create flow (agent.clutchcall.dev) reserves the agent handle. It mints the media key/secret (TELEQUICK_MEDIA_KEY / TELEQUICK_MEDIA_SECRET, which AgentConfig.from_env() reads). It writes the VENDOR_BRIDGE config that routes calls to the handle. Attach the agent to a DID or trunk (see SIP Trunking). Then dial.
4

Originate calls without an answer URL

Twilio’s calls.create takes a TwiML url to control the answered call. Here you pass the agent instead. On answer, the engine connects the callee to your worker. Management operations use an mpk_ API key (console → Settings → API keys) against POST /mcp. The SDK wraps that endpoint:
voice.calls.transfer and voice.calls.hangup cover mid-call control in the same way.

Webhooks instead of status callbacks

Register one endpoint for the voice vertical and select your events:
Deliveries are signed Stripe-style: an X-Clutchcall-Signature header of the form t=<epoch>,v1=<hmac_sha256(secret, "t.body")>. Check each delivery with telequick_agents.webhooks.verify_signature(secret, header, body) before you trust the payload.

Keep your Twilio numbers

You do not have to port numbers on day one. A Twilio Elastic SIP trunk can point at the platform as a carrier path. Twilio keeps ownership of the numbers. ClutchCall terminates the SIP legs. The same external-agent routing applies. See SIP Trunking for trunk setup. See the Twilio migration guide for the full cutover sequence.

Troubleshooting

The Media Streams payload was 8 kHz μ-law. The Call API speaks 8 kHz pcm16. If your code still base64-decodes into μ-law bytes and sends them without conversion, every sample is wrong. Convert with telequick_agents.g711.ulaw_to_pcm / pcm_to_ulaw at the provider boundary only.
A truncate of the Realtime response only stops generation. Also call await call.clear(). It drops the audio that is already queued for playout. The sample’s "clear" event did this on Twilio.
Check the signature against the exact raw request body. A re-serialized JSON body (reordered keys, changed whitespace) computes a different HMAC.

From Twilio

The migration path: numbers, trunks, and rollout order.

LiveKit integration

The same external-agent seam for LiveKit Agents workers.

Run a LiveKit Agent on the Transport

The end-to-end external-agent recipe on the same seam.

SIP Trunking

Point an Elastic SIP trunk (or any carrier) at the platform.