WebTransport is how a browser reaches the voice stack over raw QUIC. It gives you a single bidirectional session to the relay — no TCP, no WebSocket framing, no separate signalling socket — and every voice track, control message, and data stream for a call rides inside it. You open one session and publish or subscribe; the transport handles the rest. The stack terminates QUIC, HTTP/3, and WebTransport on one :443. There is no second media port to open, no UDP range to whitelist beyond 443, and no raw protocol port to reason about — the browser dials https://relay.telequick.dev and gets a WebTransport session on the same endpoint that serves everything else.

Open a session

You never touch the WebTransport object directly. Every SDK ships a MoqtClient that opens the session, negotiates the protocol version, and keeps it alive across drops. Call connect and start publishing.
import { MoqtClient } from "@telequick/sdk/moqt";

const client = await MoqtClient.connect(
  "quic://relay.telequick.dev",
  workspaceToken,
  (state, reason) => console.log("transport", state, reason ?? ""),
);
// client is live — publish or subscribe voice/<sid>/{uplink,downlink}
A few things happen under the hood that you can rely on:
  • The quic:// URL is rewritten to https:// and the tenant token is attached as a query parameter, verified at the relay’s control plane before any track flows.
  • The client advertises the MoQT version as the WebTransport subprotocol (moqt-16). The relay refuses a session that doesn’t offer it, so this is not optional — the SDK always sends it for you.
  • connect returns as soon as the session is up and auto-reconnects with capped backoff if the link drops, replaying every publication and subscription. The onState callback reports Connecting / Connected / Reconnecting / Closed / Failed.
The browser SDK is WebTransport-only today — if QUIC is blocked (some corporate proxies, UDP-filtered networks), the browser client cannot fall back on its own. The QUIC→WebSocket fallback ladder currently ships only in the native SDK cores. For those environments, use the WebRTC media leg described in Browser compatibility.

How MoQT rides the session

MoQT (Media-over-QUIC Transport) is what carries your named tracks — voice/<sid>/uplink and voice/<sid>/downlink for a call — inside the one WebTransport session. It uses two of WebTransport’s primitives:
  • A control stream — a single bidirectional stream carries MoQT setup and the subscribe/publish signalling. This is where the client and relay agree on versions, announce namespaces, and track subscriptions.
  • Object streams — each track group opens its own unidirectional stream of objects. Audio rolls over to a fresh group roughly once a second, so a live call is a rolling series of short-lived streams, each carrying ~1 s of encoded audio objects. Putting each group on its own stream means a lost or slow group can’t head-of-line-block the next one — the transport keeps moving.
You don’t manage streams by hand. You publish objects to a track and subscribe to a track by name; the SDK maps that onto the streams above. See Sessions, calls, tracks & streams for the track model.

Streams vs datagrams

WebTransport offers two ways to move bytes, and they trade reliability for latency differently:
PrimitiveDeliveryOrderingVoice uses it for
StreamsReliable — retransmitted until deliveredOrdered within a streamAll voice media today. Each track group is one stream, so audio objects arrive in order and complete, while separate groups stay independent.
DatagramsUnreliable — sent once, may be droppedUnorderedNot used by the browser voice path. A WebTransport capability MoQT can carry single-object payloads over; reserved for latency-critical, loss-tolerant data.
The per-group stream design gives you the best of both: reliable, in-order audio within the ~1 s a group lives, without one bad group stalling the whole call. You get datagram-like isolation between groups and stream reliability inside them — no datagram loss handling to write yourself.

The server certificate hash

In production the relay presents a normal CA-issued certificate with multi-brand SNI, so the browser trusts it like any HTTPS origin — you pass no hash, and the serverCertificateHashes machinery never engages. For local development against a self-signed relay, WebTransport lets you pin the exact certificate by its SHA-256 fingerprint instead of installing a CA. Pass the base64 hash and the SDK wires it into the session:
const client = await MoqtClient.connect(
  "quic://127.0.0.1:4443",
  devToken,
  undefined,
  { serverCertificateHash: "Base64Sha256OfTheCert=" },
);
The certificate-hash path has strict rules the browser enforces:
  • The certificate must be ECDSA (not RSA) with a validity window of 14 days or less — regenerate it on that cadence for local dev.
  • Dial 127.0.0.1, not ::1. The IPv6 loopback breaks the WebTransport handshake.
  • It only covers self-signed dev relays. Never ship a pinned hash to production — use a real certificate.

Transport to web & apps

How browser and mobile clients reach the voice stack over QUIC.

Browser audio capture

Capture and encode the microphone, then publish it over this session.

One-line WebRTC diversion

Divert an existing WebRTC audio pipeline onto QUIC in one call.

Browser compatibility

What works where, and the fallback when QUIC is blocked.