# Transport & Track API

> The MoQT primitive under the Voice SDK: publish and subscribe caller audio on voice/`<sid>`/{uplink,downlink}, with capability tags and namespace auth.

The `AudioBridge` helper in the [SDK](/modalities/voice/sdk/javascript) is a
thin wrapper. Under it, every call's audio is two live MoQT tracks that the
relay fans out. This page is the surface below the SDK. It documents the raw
`publish_audio` / `subscribe_audio` primitive, the exact namespace layout for a
call, the capability tags that route a track, and how the relay authorizes you
to touch a namespace. Use this surface when you bridge your own runtime, tape a
call into a recorder, or connect a human agent to a live caller.

Read [Realtime Tracks (MoQT)](/concepts/realtime-tracks) first for the general
model (namespace + name + capability, connect/backoff/reconnect, track kinds).
Everything here is that model, pinned to voice.

## The two tracks of a call

The `call_sid` keys every call (see
[Sessions, calls, tracks & streams](/modalities/voice/concepts/sessions-calls-tracks-streams)).
The call's audio lives under a per-call namespace, split by **direction**, with
the track name `audio`:

| Namespace                 | Direction            | Carries                                   |
| ------------------------- | -------------------- | ----------------------------------------- |
| `voice/<sid>/uplink`      | caller → cloud       | what the caller says                      |
| `voice/<sid>/downlink`    | cloud → caller       | what plays back to the caller             |

The RTP media plane publishes the caller's audio to `uplink`. It subscribes to
`downlink` for playback. The track that you publish and the track that you
subscribe to depend on your side:

- **Agent / server side** (the common case) — subscribe to `uplink` to hear the
  caller. Publish `downlink` to talk back.
- **Browser-caller side** — the mirror: subscribe to `downlink` to play cloud
  audio. Publish `uplink` with captured mic frames.

> **NOTE:**
> Uplink and downlink are **distinct namespaces on purpose**. A single track
> would feed the agent its own output. The same split lets a human agent or a
> supervisor subscribe to `uplink` alone and hear only the caller (see
> [Handoffs](/modalities/voice/concepts/handoffs)).

## Publish & subscribe

`publish_audio(ns, name, …)` returns a publication. The publication's
`write(ts_us, frame)` sends one encoded audio frame (e.g. a 20 ms Opus packet).
Objects roll into a new group about once a second.
`subscribe_audio(ns, name, on_frame)` delivers each object back as
`(ts_us, frame)`. Hold both handles for the life of the call. A dropped
subscription frees the native callback. A dropped publication stops the track.

```ts
import { MoqtClient } from "clutchcall_connect/moqt";

// One WebTransport session to the relay carries every track for the call.
const client = await MoqtClient.connect(
  "https://relay.firstbyte.fast/moq",
  apiKey, // tenant token, verified at the relay
);

// Agent side: hear the caller, talk back.
client.subscribeAudio(`voice/${sid}/uplink`, "audio", (tsUs, frame) => {
  asr.push(frame); // caller audio → your runtime
});

const downlink = client.publishAudio(`voice/${sid}/downlink`, "audio", {
  capability: "voice/opus",
  sampleRate: 48000,
  channels: 1,
  frameMs: 20,
});
tts.onChunk((opus) => downlink.write(BigInt(Date.now()) * 1000n, opus));
```

```python
from clutchcall.moqt import MoqtClient

client = MoqtClient.connect(f"moq://relay.firstbyte.fast/voice/{sid}", token=api_key)

# Agent side: hear the caller, talk back.
client.subscribe_audio(f"voice/{sid}/uplink", "audio",
                       on_frame=lambda ts, frame: asr.push(bytes(frame)))

downlink = client.publish_audio(f"voice/{sid}/downlink", "audio",
                                capability="voice/opus",
                                sample_rate=48000, channels=1, frame_ms=20)
tts.on_chunk(lambda opus: downlink.write(ts_us(), opus))
```

- **`ns`** (`string`, required) — The per-call track namespace, `voice/<sid>/uplink` or `voice/<sid>/downlink`. `<sid>` is the `call_sid`.

- **`name`** (`string`, required) — The track name within the namespace — `audio` for voice legs.

- **`capability`** (`string`) — A free-form routing intent (see [Capability tags](#capability-tags)). Voice legs use `voice/<codec>`, e.g. `voice/opus`.

- **`sampleRate`** (`number`, default `48000`) — The codec sample rate of the frames that you write. Opus stays `48000`. Narrowband PCM/G.711 legs use `8000`.

- **`channels`** (`number`, default `1`) — The channel count. Voice is mono.

- **`frameMs`** (`number`, default `20`) — The frame duration per object. 20 ms is the wire cadence for Opus and G.711.

### Raw objects vs. decoded frames

The RTP media plane publishes the caller's audio on `uplink` as **raw
8 kHz PCM16 LE** objects — the runtime's internal audio format — with no SDK
frame wrapper. To consume those bytes verbatim, use `subscribe_raw`. It gives
you each MoQT object's payload untouched:

```ts
client.subscribeRaw(`voice/${sid}/uplink`, "audio",
  (groupId, objectId, pcm16) => { /* 160 samples per 20 ms @ 8 kHz */ });
```

Use `subscribe_audio` when the other side is an SDK publisher (frames carry a
timestamp header). Use `subscribe_raw` for engine-published raw tracks. See the
[audio contract](/modalities/voice/concepts/sessions-calls-tracks-streams) for
the 8 kHz PCM16 / Opus / G.711 details.

## Capability tags

A **capability** is an open-ended intent string attached to a track. The relay
and the in-engine modules route on it: "route on intent, not on media kind."
Voice legs tag themselves `voice/<codec>`, where `<codec>` is one of the SDK's
audio codecs:

| Codec value   | On the wire                    |
| ------------- | ------------------------------ |
| `opus`        | Opus 48 kHz mono, 20 ms        |
| `pcm16`       | linear PCM16                   |
| `g711_ulaw`   | G.711 µ-law (PT 0)             |
| `g711_alaw`   | G.711 A-law (PT 8)             |

So a browser caller publishes `voice/opus`. A passthrough leg off a µ-law trunk
publishes `voice/g711_ulaw`. The capability is **informational for the track
itself**. Subscribers select which namespace/name to receive. The tag tells the
routing layer *what* the bytes are, so the routing layer can hand them to the
module that registered for that intent (an ASR node, a recorder, a
`media.passthrough` relay).

> **TIP:**
> Two publishers can carry different capabilities on the same namespace. Set the
> tag to the actual codec that you write. A mislabeled leg goes to a decoder that
> cannot parse it.

## Namespace authorization

There is no separate ACL call. Authorization rides the **connect token**:

1. **Present your tenant token at connect**
Pass your workspace API key as the token to `connect`. The SDK sends the key as
a URL query parameter to `relay.firstbyte.fast`. The relay control plane
verifies the key before the MoQT session opens.

2. **The relay scopes you to your workspace's calls**
A verified session can publish and subscribe to `voice/<sid>` namespaces for
calls that belong to your workspace. `<sid>` values are unguessable per-call
keys. The relay rejects a namespace outside your tenant scope. Cross-tenant
subscription is not possible.

3. **Namespaces are non-empty tuples**
MoQT namespaces are tuples with at least one element. When you subscribe to a
namespace that no publisher announced yet, the relay **holds** the subscribe.
The relay delivers it when the publisher appears. For multi-participant rooms,
the SDK scopes each participant under a `[room, identity]` tuple. A single
call's audio lives under the flat `voice/<sid>/…` namespace.

> **WARNING:**
> The token is a workspace-scoped credential. Mint short-lived tokens for browser
> clients. Do not ship a long-lived API key into a page. Treat `voice/<sid>` as
> sensitive: anyone with a valid token for your workspace and the `sid` can join
> the audio.

## Transport & runtimes

Tracks ride **MoQT over WebTransport on the single QUIC/HTTP-3 `:443` plane**.
Browsers negotiate the `moqt-16` WebTransport subprotocol. The relay handles
connect and capped-backoff reconnect. The relay also re-establishes every
publication and subscription for you.

Two runtimes sit behind one API. The **native SDK cores** (Python, Go, Rust,
and the C++ FFI, `clutchcall_moqt_ffi`) speak QUIC. They carry a
**QUIC → WebSocket fallback ladder** for UDP-blocked networks. The
**TypeScript SDK** is a standalone WebTransport implementation and is
**WebTransport-only today**. There is no browser WS fallback yet. Hold the two
as separate conformance surfaces. The publish/subscribe APIs match, but the
queueing and unsubscribe lifecycle can differ.

> **NOTE:**
> For browser callers, you rarely call this primitive directly. The SDK's
> WebRTC-diversion capture wraps `publish_audio` with encoded-Opus tapping. See
> [Browser audio capture](/modalities/voice/transport-web/browser-audio-capture)
> and [WebTransport](/modalities/voice/transport-web/webtransport).

## Related

  - **[Realtime Tracks (MoQT)](/concepts/realtime-tracks)** — The general publish/subscribe model that this page pins to voice.
  - **[Sessions, calls, tracks & streams](/modalities/voice/concepts/sessions-calls-tracks-streams)** — How call_sid keys the SIP dialog, the media session, and every track.
  - **[AudioBridge SDK](/modalities/voice/sdk/javascript)** — The high-level helper that wraps these tracks per call.
  - **[Handoffs](/modalities/voice/concepts/handoffs)** — Subscribe to voice/&lt;sid&gt;/uplink to put a human on a live caller.
