A session is the live transport connection that carries one call’s audio. Your code holds the session open while a caller and an agent talk. The session is a MoQT session over WebTransport. A relay token authenticates the session. The session’s scope is a room-shaped namespace keyed on the call_sid. This page documents how the SDK authenticates and brings up that session. This is the plumbing under AudioBridge.attach. You rarely wire it by hand, but you need the shapes to reason about auth, tenancy, and reconnection.
There is no public REST API for sessions. The control plane is a set of tRPC-style procedures on the control-plane host. The SDK calls these procedures for you. You authenticate with an API key. The SDK does the procedure calls and the relay handshake. The shapes below are the real shapes that the SDK sends. Treat them as the contract, not as endpoints to hand-roll.

Two planes, one credential

A session touches two planes. The same workspace API key authorizes both: The control plane is a request/response surface (queries and mutations). The data plane is a long-lived session that auto-reconnects. To get onto the data plane, first ask the control plane where the relay lives. The control plane also proves that you can attach.

Establish a session

1

Authenticate the control plane

Construct the client with your workspace API key and org id. The SDK sends the key as Authorization: Bearer <apiKey> on every control-plane call. The org id scopes every procedure to your tenant.
2

Resolve the relay edge

Before the SDK opens a session, it asks the control plane which relay edge to dial. This is a query. The SDK sends it, but the shape is worth knowing. The query geo-routes you to the nearest edge. It also carries the fields that the WebTransport handshake needs.
string
required
Your tenant id. It scopes the response to your workspace.
string
Pin a specific relay edge (e.g. "us", "uk"). Omit this field to let the server geo-route to the nearest edge.
Response:
boolean
false when the tenant has no QUIC/relay entitlement. The rest of the fields are then absent. Fall back to a control-plane-only integration.
string
The MoQT relay endpoint to open the session against. The server already selected the region.
string
The edge that the server chose. The response echoes it for telemetry and for a pin on reconnect.
string | null
A base64 SHA-256 certificate fingerprint for self-signed dev relays. This is null in production. In production, the relay presents a public-CA certificate, and no fingerprint pin is necessary.
On managed cloud, the relay runs on a public-CA certificate, so relayCertHashBase64 is null. You pass a cert hash only when you point the SDK at a self-signed relay in local development.
3

Open the relay session

With the relay URL and a token, the SDK opens the MoQT session. Under the hood this is MoqtClient.connect(url, token, onState, opts). The SDK appends the token to the URL as a ?token= query. The relay’s control plane verifies the token. The WebTransport handshake advertises the moqt-16 subprotocol. The relay closes the session if the version subprotocol is missing.
The session address is the call_sid. The SDK connects to moq://<relayHost>/voice/<call_sid> and binds the uplink / downlink tracks under that namespace. One call_sid is the whole addressing story. The SIP dialog, the media session, the agent session, the telemetry, and this transport session all key off it (see Sessions, calls, tracks & streams).
4

Observe session state

The session reconnects transparently with capped backoff. The SDK re-establishes publications and subscriptions on every reconnect. Subscribe to the state machine to drive UI and to know when audio actually flows.
The first connect is in progress.
The session is up; the tracks are live.
The transport dropped. The SDK retries with capped exponential backoff. On success, the SDK replays your pubs/subs.
You (or the peer) ended the session cleanly.
The initial connect failed. The state carries a reason string.

Namespace auth (room scope)

The relay does not give a token holder the whole tenant. Authorization is namespace-scoped: a session can publish and subscribe only within the room-shaped namespace tuple that the relay authorized it for. For voice, that room is voice/<call_sid>. The two tracks inside it are fixed by role: The distinct uplink and downlink tracks prevent a leg from hearing itself. An attach subscribes to uplink and publishes downlink. attachCaller (the browser-caller mirror) does the reverse.
Namespaces are tuples with a room scope, never a single flat identifier. A valid namespace carries at least a one-element prefix and a suffix. The relay rejects a zero-element namespace. This is why every voice session address is voice/<call_sid>/<track> and never a bare id: the relay authorizes against the room prefix.

Connect options

These are the knobs that the SDK passes to the relay session. attach sets the media-relevant knobs for you. These knobs are the honest surface if you drop to the transport client directly.
string
The relay token. The relay control plane verifies it. The SDK sends it as a ?token= query on the WebTransport URL. The Voice SDK forwards your workspace API key here.
string
A base64 SHA-256 certificate fingerprint for a self-signed dev relay. Leave this unset in production (relayCertHashBase64 from gatewayConnectInfo is null there).
(state, reason?) => void
A callback for the ConnectionState machine above.
boolean
Refuse to connect unless the browser exposes the standard WebRTC Encoded Transform API (RTCRtpScriptTransform). Media-sending browser sessions set this flag, so the encoded-frame rule holds up front instead of failing later at capture time. Leave the flag unset for receive-only or non-media sessions. See browser audio capture.
Fallback honesty. In the browser / TypeScript SDK, a session is WebTransport-only today. If WebTransport is unavailable, the SDK does not silently degrade. The QUIC→WebSocket fallback ladder currently ships only in the native SDK core (Python, Go, Rust, Java, .NET wrappers). The native core stays on a WebSocket rung when QUIC is blocked.

Sessions vs. calls

Keep the two concepts clearly apart:
  • A call is the SIP dialog and its lifecycle — originate, ring, answer, transfer, hangup. You drive a call with the Calls API (voice.calls.*), which returns CallData keyed by sid.
  • A session is the transport connection that carries that call’s audio. The same sid addresses it. You open a session with the Transport / AudioBridge API.
The typical flow:
  1. Originate a call (control plane).
  2. Take the returned sid.
  3. Attach a session (data plane) to move audio.
For the server-side “drop-in agent” pattern, you skip the manual session entirely. voice.agents.attach tells the engine to connect the bridge in-process, and no client session opens. See the runtime session lifecycle for the engine’s view of that flow.

Transport / AudioBridge

Open and drive the audio session for a call.

Calls

Originate, transfer, and hang up — the call control plane.

Sessions, calls, tracks & streams

The object model: how call_sid, namespaces, and tracks relate.

Errors

The error shapes that the control plane and the session return.