# JavaScript / TypeScript

> The typed Voice SDK for JS/TS: the Voice client, Calls/Call, AudioBridge, Agents, and the browser softphone helpers.

The Voice SDK lives on its own subpath. Import the `Voice` client for the
control and data planes, and the optional browser helpers when you are building
a softphone in the browser.

```ts
import { Voice } from "clutchcall_connect/voice";
// browser softphone helpers live on the moqt subpath:
import { captureMicrophone, OpusPlayer } from "clutchcall_connect/moqt";
```

The surface splits into three jobs: **place and control calls**
(`Voice.calls`), **move audio frames** (`Voice.audioBridge`), and **hand a call
to a server-side AI agent** (`Voice.agents`). This page is the TypeScript
reference. The same shape ships in
[Python](/modalities/voice/sdk/python), [Go](/modalities/voice/sdk/go), and
[Rust](/modalities/voice/sdk/rust).

> **NOTE:**
> This is a **callback surface, not an `EventEmitter`**. There is no `join()` or
> `say()`. You originate a call, then feed and drain audio through
> `publishDownlink` / `onUplink` (or hand the whole call to an agent). See
> [SDK Events](/modalities/voice/sdk/events) for the one place where events
> appear (the human-agent control channel).

## Construct the client

The top-level `Voice` client gives you three scoped helpers: `calls`,
`audioBridge`, and `agents`. Each helper carries your org id and the transport.

```ts
const v = new Voice({
  baseUrl: "https://engine.firstbyte.fast",   // control-plane origin
  apiKey:  process.env.CLUTCHCALL_CREDENTIALS!,
  orgId:   "org_abc",
});

v.calls;        // control plane  (place / fetch / transfer / hangup)
v.audioBridge;  // data plane     (uplink / downlink audio frames)
v.agents;       // AI-agent attach
```

- **`baseUrl`** (`string`, required) — Control-plane origin (no trailing slash). Requests go over HTTPS with   `Authorization: Bearer <apiKey>`.
- **`apiKey`** (`string`, required) — API key with `voice:*` scopes.
- **`orgId`** (`string`, required) — Default org / tenant id applied to every call.
- **`relayHost`** (`string`) — Relay hostname for the audio bridge. Defaults to `relay.firstbyte.fast`.
- **`fetch`** (`(input, init) => Promise<Response>`) — Override the `fetch` implementation (for example, in Node before global `fetch`).
- **`webTransport`** (`WebTransportFactory`) — Inject a custom WebTransport factory for the audio bridge (mainly for Node   tests).

The constructor throws a `VoiceError` if `baseUrl`, `apiKey`, or `orgId` is
missing.

## Place a call and move audio

This is the end-to-end shape of a server-side voice integration: originate,
attach the bridge, drain caller audio into your ASR, and push synthesized
audio back.

  1. **Originate the call**
`calls.originate()` places an outbound call over a SIP trunk and resolves to
    a `Call` handle once the call is `dialing`.

    ```ts
    const call = await v.calls.originate({
      to:      "+15551234567",
      from:    "+15558675309",
      trunkId: "trunk_main",
      ringTimeoutSec: 30,        // optional, server-clamped 5..120
    });
    console.log(call.sid, call.status);
    ```
  2. **Attach the audio bridge**
`audioBridge.attach()` opens the bidirectional bridge **as the server**:
    it subscribes the caller's audio (uplink) and publishes audio back
    (downlink). Pass `onUplink` to receive caller frames.

    ```ts
    const bridge = await v.audioBridge.attach(call.sid, {
      codec: "opus",                       // default
      onUplink: (frame, tsUs) => myAsr.feed(frame),
    });
    ```
  3. **Push audio back to the caller**
Send each encoded frame from your TTS onto the downlink track.

    ```ts
    myTts.onChunk((opus) => bridge.publishDownlink(opus));
    ```
  4. **Close when the call ends**
Tear down both tracks and the underlying session.

    ```ts
    await call.hangup();
    await bridge.close();
    ```

> **WARNING:**
> Hold the `AudioBridge` handle for the whole call. If it is garbage-collected,
> the tracks go silent. Call `close()` explicitly when the call ends.

## `Calls` — control plane

Reachable as `v.calls`. Originates calls and fetches their current state. Every
method resolves against the control-plane API and returns a `Call`.

### `calls.originate(args) → Promise<Call>`

Place an outbound call over a SIP trunk. Resolves once the call is `dialing`.

```ts
const call = await v.calls.originate({
  to:      "+15551234567",
  from:    "+15558675309",
  trunkId: "trunk_main",
  agent:   "healthcare-assistant",  // optional: engine attaches the bridge on answer
  ringTimeoutSec: 30,               // optional, server-clamped 5..120
});
```

- **`to`** (`string`, required) — E.164 destination.
- **`from`** (`string`, required) — Caller-id, E.164.
- **`trunkId`** (`string`, required) — SIP trunk to route over.
- **`agent`** (`string`) — AI agent id to attach automatically on answer.
- **`ringTimeoutSec`** (`number`) — Seconds to ring before giving up. Server-clamped 5..120, default 30.

### `calls.get({ sid }) → Promise<Call>`

Fetch the current state of a call by its `sid`. Use it to poll `status` after
`originate()`. The control plane is request/response, so re-fetch to observe a
call as it moves through its lifecycle.

```ts
const call = await v.calls.get({ sid });
```

## `Call` — one call handle

`originate()` and `get()` return a `Call`. It has read-only fields plus two
mutating methods.

| Field       | Type         | Notes                                          |
| ----------- | ------------ | ---------------------------------------------- |
| `sid`       | `string`     | The `call_sid` — the only id you need to address audio. |
| `status`    | `CallStatus` | `dialing` … `completed` (see [Types](#types)). |
| `to`        | `string`     | E.164 destination.                             |
| `from`      | `string`     | Caller-id.                                     |
| `startedAt` | `string`     | ISO-8601 start time.                           |
| `trunkId`   | `string?`    | SIP trunk.                                     |
| `agent`     | `string?`    | Attached AI agent id, if any.                  |

### `call.transfer(args | string) → Promise<void>`

Transfer the call to a PSTN number **or** re-attach a different agent. Provide
exactly one of `to` / `agent`. A bare string is shorthand for `{ to }`. The
engine executes a PSTN transfer as a `REFER` on the SIP leg
([Telephony API](/modalities/voice/api/telephony)).

```ts
await call.transfer({ to: "+15557654321" });   // forward to PSTN
await call.transfer("+15557654321");           // shorthand
await call.transfer({ agent: "billing-bot" }); // re-attach an agent
```

The method throws a `VoiceError` if you supply neither `to` nor `agent`.

### `call.hangup() → Promise<void>`

End the call and drop both audio tracks.

```ts
await call.hangup();
```

## `AudioBridge` — data plane

Reachable as `v.audioBridge` (an `AudioBridgeFactory`). It opens the
bidirectional audio bridge for one call over our media-over-QUIC transport.
It returns a handle that you hold for the call's lifetime. The bridge maps to
the `voice/<sid>/{uplink,downlink}` track convention described in
[Realtime Tracks](/concepts/realtime-tracks).

### `audioBridge.attach(callSid, opts) → Promise<AudioBridge>`

Open the bridge **as the server**: subscribe to the caller's audio (uplink)
and publish audio back (downlink). `onUplink` is required.

```ts
const bridge = await v.audioBridge.attach(call.sid, {
  codec: "opus",          // default
  sampleRate: 48000,      // default
  channels: 1,            // default
  frameMs: 20,            // default
  onUplink: (frame, tsUs) => myAsr.feed(frame),
});

// push synthesized audio back to the caller:
myTts.onChunk((opus) => bridge.publishDownlink(opus));
```

- **`onUplink`** (`(frame: Uint8Array, timestampUs: bigint) => void`, required) — Callback for inbound caller audio, fired once per encoded frame.
- **`codec`** (`AudioCodec`) — Default `opus`. See [Types](#types).
- **`sampleRate`** (`number`) — Default `48000`.
- **`channels`** (`number`) — Default `1`.
- **`frameMs`** (`number`) — Frame duration in ms. Default `20`.

### `audioBridge.attachCaller(callSid, opts) → Promise<AudioBridge>`

The **browser-caller** mirror of `attach()`: subscribe to the downlink
(cloud → caller) and publish the uplink (mic → cloud). Pass `onDownlink` to
receive audio for playback. This is the softphone path. Pair it with
[`captureMicrophone`](#browser-softphone-helpers) and `OpusPlayer`.

```ts
const bridge = await v.audioBridge.attachCaller(call.sid, {
  codec: "opus",
  onDownlink: (frame, tsUs) => player.push(tsUs, frame),
});
```

### `AudioBridge` methods

| Method                                   | Direction        | Notes                                                    |
| ---------------------------------------- | ---------------- | -------------------------------------------------------- |
| `publishDownlink(frame, timestampUs?)`   | server → caller  | Push one encoded frame to the caller (e.g. 20 ms Opus).  |
| `publishUplink(frame, timestampUs?)`     | caller → cloud   | Browser-caller side; push one encoded mic frame.         |
| `onUplink(cb)`                           | —                | Setter for the inbound-caller consumer (server side).    |
| `onDownlink(cb)`                         | —                | Setter for the inbound-cloud consumer (browser side).    |
| `close()`                                | —                | Tear down both tracks and the underlying session.        |
| `callSid`                                | field            | The `sid` this bridge belongs to.                        |

`timestampUs` is an optional `bigint`. If you omit it, the SDK stamps a
monotonic microsecond clock. The `onUplink` / `onDownlink` setters keep the
public surface stable. The consumer callback that you pass to `attach()` /
`attachCaller()` is the one that fires per frame.

> **NOTE:**
> The browser/TS SDK moves audio over WebTransport only. The QUIC→WebSocket
> fallback ladder currently ships in the native SDK cores (Python, Go, Rust, and
> the other wrappers), not in the browser build. Plan for WebTransport-capable
> clients when you build in the browser.

## `Agents` — AI-agent attach

Reachable as `v.agents`. Bind a server-side speech-to-speech agent to a live
call. The engine wires the audio bridge end-to-end, so you do **not** open an
`AudioBridge` yourself.

### `agents.attach(callSid, agent) → Promise<void>`

```ts
await v.agents.attach(call.sid, "healthcare-assistant");
```

> **TIP:**
> You can attach an agent in two ways. Pass `agent` to `originate()`, and the
> engine attaches it on answer. Or call `agents.attach()` against a live `sid` —
> for example, to hand a call from a human to a bot, or to swap one bot for
> another. See the
> [agent runtime overview](/modalities/voice/runtime/overview) for how to
> configure the attached agent.

## Browser softphone helpers

These helpers ship on the `moqt` subpath. They turn a microphone into encoded
Opus and play encoded Opus back, so the softphone never touches a WebRTC
transport for media. The browser's own audio pipeline (echo cancellation, gain
control, noise suppression, Opus encode) runs behind a loopback capture graph.
The SDK taps the **encoded** frames and sends the payloads over the
media-over-QUIC transport.
[Browser Audio Capture](/modalities/voice/transport-web/browser-audio-capture)
and [WebRTC diversion](/modalities/voice/transport-web/webrtc-diversion)
cover the mechanics.

### `captureMicrophone(publication, opts?) → Promise<MicCapture>`

This helper captures the mic and runs echo cancellation, gain control, and
noise suppression. It encodes the audio to Opus and writes each **encoded**
frame to a MoQT audio publication. The returned `stop()` tears down the
capture graph (it does not close the publication). The simplest softphone loop
forwards captured frames into the bridge with `publishUplink`:

```ts
const bridge = await v.audioBridge.attachCaller(call.sid, {
  codec: "opus",
  onDownlink: (frame, tsUs) => player.push(tsUs, frame),
});

// forward each captured mic frame onto the uplink track
const mic = await captureMicrophone(
  { write: (tsUs, frame) => bridge.publishUplink(frame, tsUs) } as any,
  { audioConstraints: { echoCancellation: true, autoGainControl: true, noiseSuppression: true } },
);
// later:
mic.stop();
```

> **WARNING:**
> `captureMicrophone` hard-gates on the browser's encoded-transform support and
> **throws** if that support is unavailable. It never silently falls back to a
> plain WebRTC media transport. Check
> [Browser compatibility](/modalities/voice/transport-web/browser-compatibility)
> before you ship.

### `OpusPlayer`

`OpusPlayer` decodes received Opus with WebCodecs and renders it through an
audio ring buffer (padded with silence on underrun). Construct it with an
`AudioContext`. Call `start()` once, then `push()` each frame you receive.

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

const ctx = new AudioContext();
const player = new OpusPlayer(ctx, { sampleRate: 48000, channels: 1 });
await player.start();

// feed it the frames from the downlink subscription:
// onDownlink: (frame, tsUs) => player.push(tsUs, frame)

player.close();
```

## Types

| Type         | Values                                                                             |
| ------------ | ---------------------------------------------------------------------------------- |
| `CallStatus` | `dialing` \| `ringing` \| `in_progress` \| `completed` \| `failed` \| `no_answer`  |
| `AudioCodec` | `opus` \| `pcm16` \| `g711_ulaw` \| `g711_alaw`                                     |

Voice is **audio-only**. There are no video codecs on this surface. Phone legs
use G.711 on the wire; the engine transcodes them to the codec you request.
Browser legs are Opus. Errors thrown by the SDK are instances of `VoiceError`.

The control-plane surface is request/response. There is no socket of call
events, so track a call's lifecycle with repeated `calls.get({ sid })` calls.
The **data plane** is event-driven. The `onUplink` / `onDownlink` callbacks
fire per audio frame. The underlying session auto-reconnects and replays the
publish/subscribe on link loss.

## Related

  - **[SDK Events](/modalities/voice/sdk/events)** — The human-agent control channel — the one `EventEmitter` surface.
  - **[Calls API](/modalities/voice/api/calls)** — The control-plane routes behind `calls` and `Call`.
  - **[Browser audio capture](/modalities/voice/transport-web/browser-audio-capture)** — How the softphone taps encoded Opus onto QUIC.
  - **[Python / Go / Rust](/modalities/voice/sdk/python)** — The same Voice client in the other languages.
