call.initiatedarrives. You sendanswer.call.answeredarrives. You sendspeak.call.speak.endedarrives. You send the next command.
call_control_id, with a webhook
round-trip in between. Thus your app is a state machine, and every state
transition crosses the network twice.
On ClutchCall, the whole flow is one function. Your code runs as an
external agent. The external agent is 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 coroutine. Audio flows in both directions as 20 ms pcm16 frames at
8 kHz, in-process, with no command round-trips.
What maps to what
Port the voicemail example
The runnable starter is a port of Telnyx’svoicemail-python Call Control
example. It ships in the provider starters repo next to the
telequick-agents SDK. Upstream, it is a Flask server plus a webhook state
machine. Here, it is one coroutine:
1
Install the SDK
2
Collapse the state machine into a handler
agent.py
voice.recording.ready
webhook with the file. You do not poll for call.recording.saved.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, which
AgentConfig.from_env() reads. It writes the VENDOR_BRIDGE config.
Route a DID to the agent. Then dial the DID.4
Keep lifecycle webhooks — as observability
In Call Control, webhooks were the control flow. Here they only inform.
Register one time with an Deliveries carry an
mpk_ management key (console → Settings → API
keys) against POST /mcp through the SDK wrapper:X-Clutchcall-Signature header
(t=<epoch>,v1=<hmac_sha256(secret, "t.body")>). Check each delivery with
telequick_agents.webhooks.verify_signature before you trust the payload.
Outbound dialing and transfer are voice.calls.originate /
voice.calls.transfer on the same API.Keep your Telnyx trunk
Numbers do not have to move first. Point a Telnyx SIP trunk at the platform as a carrier path. Telnyx keeps the numbers. ClutchCall terminates the SIP legs. The same external-agent routing applies. See SIP Trunking.Troubleshooting
You're waiting for an event that never comes
You're waiting for an event that never comes
There is no
call.speak.ended here. Nothing blocks on webhooks. Sequence
your flow in the coroutine itself: send_audio → flush → await playout.
Then read call.audio().The greeting is distorted or the wrong speed
The greeting is distorted or the wrong speed
send_audio is a raw passthrough at the call’s sample rate. Resample your
prompt files to 8 kHz mono 16-bit PCM offline. Do not send wideband
samples onto a telephony leg.The worker registers, but calls never arrive
The worker registers, but calls never arrive
vendor_room in the agent config must exactly match the worker’s handle
(TELEQUICK_AGENT). The DID must route to that agent. Also check outbound
UDP/443. Media rides QUIC datagrams. A TCP-only network stalls audio after
a successful registration.Related
Plivo integration
The same port for the answer-URL/XML model.
LiveKit integration
The external-agent seam with a full agent framework on top.
Run a LiveKit Agent on the Transport
The end-to-end external-agent recipe on the same seam.
SIP Trunking
Trunk setup, which includes how to point your Telnyx trunk at the platform.

