You want a voice agent on the phone, written in TypeScript, with your own STT/LLM/TTS — and you do not want to adopt a framework to get there. This recipe builds exactly that: a plain Node process that dials one outbound QUIC session to TeleQuick, receives each routed call, and streams audio in and out. Your process holds no inbound ports. TeleQuick owns the hard edge — the SIP trunk or browser leg, turn-taking, recording, analytics — and hands your worker 8 kHz PCM in, takes 8 kHz PCM back.
Already on LiveKit Agents? Keep your AgentSession verbatim and only swap the transport — see Run a LiveKit Agent on TeleQuick Transport. This recipe is the framework-free path for everyone else.

What you’ll build

1. Install

The SDK is served from our package origin, not the public registries — deps still resolve from npmjs/PyPI/crates.io as normal.
@fails-components/webtransport is the native QUIC/WebTransport client for Node (Node has no built-in one). It is an optional peer — install it wherever the worker runs. Node 20+.

2. Get media credentials

In the console, create an External agent (Agents → New → External). That flow, in one step:
  • registers a VENDOR_BRIDGE agent whose vendor_room is your agent name, and
  • mints a media key/secret (ck_…) scoped to your org.
Your worker authenticates with that key/secret and registers under the agent name; the platform routes every call for that agent to your connected worker.

3. Echo agent — prove the media path

Start with an agent that echoes the caller. It exercises the whole loop — caller → gateway → QUIC → your process → back onto the call — with no AI:
serve() connects, authenticates (HMAC over a server nonce — your secret never leaves your process), registers, and runs handler once per call. It reconnects with backoff on any drop, so presence recovers on its own across an engine restart.

Know when it’s connected

serve() runs forever, so it never “returns connected”. Read its lifecycle through the callbacks instead — onReady fires the moment the worker is authenticated and registered (present to the platform, awaiting calls), and again after every automatic reconnect; onDisconnected fires on a drop, before the retry. Drive a health check or a readiness gate off them:
On the platform side, a connected worker shows as present for its agent in the console (Agents → your external agent → status), and the worker logs worker ready (agent=…) — awaiting calls on each successful (re)connect.

4. Your real agent

Swap the echo body for your pipeline. Audio in and out is 16-bit PCM at call.sampleRate (8 kHz for telephony) — resample your provider’s audio to that rate before sendAudio. Forward each turn with sendTranscript so it lands in TeleQuick’s transcript store, analytics timeline, and voice.transcript.ready webhooks — the same history a native agent produces.
What the Call gives you:

5. Route a number and place a call

Point an inbound number at the External agent you created (Numbers → assign → your agent), then call it. With the worker running you’ll see call_id=… from=… → handler, and the caller hears your agent. To dial out to your agent, use the admin API (@telequick/admin-sdk) or the console’s outbound flow — the answered call is dispatched to your worker exactly like an inbound one.

Notes

  • No inbound ports. The worker only dials out on :443 — deploy it anywhere, including behind NAT.
  • 8 kHz pcm16 both ways. Telephony legs are 8 kHz; resample 24/48 kHz TTS down before sendAudio, or you’ll hear chipmunk audio.
  • Concurrency is free. Each call is its own Call; serve runs your handler per call on the one shared session.
  • Other languages. The identical SDK — same wire protocol, same serve(handler) / Call shape, same onReady/onDisconnected lifecycle — ships for four languages, one directory per language in the same package: Each carries an examples/echo you can run against your agent with the same four TELEQUICK_HOST / TELEQUICK_MEDIA_KEY / TELEQUICK_MEDIA_SECRET / TELEQUICK_AGENT variables. All four ship at the same version and install from the same origin:
    GOSUMDB=off is required for the Go module: the public checksum database has no entry for a module served from a private proxy. Scope it to this module (GONOSUMDB/GOPRIVATE) if you would rather not disable it globally.