# A BYO media session

Source: https://docs.usetone.ai/flows/byo-session

> What happens on the wire between Tone and your own voice stack, from dial to hang-up.

See [Bring your own voice stack](/quickstart-byo) for setup and
[Tone Media Streams](/tone-media-streams) for the protocol reference.

## The setup, once

| # | Step | Where |
|---|---|---|
| 1 | Declare your sender classification | Dashboard |
| 2 | Store the endpoint credential | Dashboard — **Developer → Secrets** |
| 3 | `PATCH /v1/numbers/{id}` with `routingMode: "byo_ws"` and `mediaEndpoint` | API |

Steps 1 and 2 are dashboard-only and will `403` with an API key, deliberately.

## Then, per call

| # | What happens |
|---|---|
| 1 | `POST /v1/calls` **without `agentId`** — sending one is a `422` |
| 2 | The compliance gate runs, exactly as it does for an agent call |
| 3 | The carrier dials |
| 4 | Tone opens a WebSocket to your `mediaEndpoint` |
| 5 | A `start` frame arrives with the call id, direction, from, to and your `customParameters` |
| 6 | `media` frames flow both ways until someone hangs up |
| 7 | Tone writes the call record and bills at the BYO rate |

Inbound needs no extra work: a BYO number answers inbound the same way.

## The endpoint

**`static`** names one `wss://` URL, used for every call with the call id
appended as a query parameter. **`webhook`** names an `https://` URL fetched per
call that answers `{"url":"wss://…"}` — which is how you route calls to different
backends.

🔴 Both must be publicly reachable. Private and link-local addresses are refused
when you save the config, and refused **again at connect time after DNS
resolution** — because the first check can be sidestepped by DNS and the second
by a config that was never validated.

## Three rules that decide whether it sounds right

- **Send small chunks**, 20–100ms. One large buffer is a caller waiting.
- **Use `mark`** to know when your audio finished playing. Guessing from byte
  counts drifts.
- **Hang up by closing the socket.** There is no separate hang-up frame.

`customParameters` merge in three layers, later winning: static number config →
per-call `variables` → identity facts (`callId`, `direction`, `from`, `to`).

## Every refusal reports a completed call

If Tone cannot reach you, the call still ends properly — it is never left hanging
in your CDR:

| `endReason` | |
|---|---|
| `refused_no_endpoint` | The number is `byo_ws` with no `mediaEndpoint` |
| `refused_endpoint_unreachable` | Your endpoint did not accept the connection |
| `refused_endpoint_forbidden` | It resolved into private address space, or refused the credential |
| `refused_media_format` | The negotiated audio format is not one we speak |
| `max_duration` | The 60-minute ceiling |

Each writes a call record with a `failed` disposition. Watch for these before
blaming the carrier.

## Two limitations

**Campaigns do not run on BYO numbers** — refused at launch rather than as
thousands of per-call errors.

**A negotiated full-stack rate does not apply.** BYO bills the
telephony-plus-platform subset; see [Billing](/billing).
