One-way audio means the call connects, both sides think they’re talking, but audio only travels one way: the caller hears the agent and you hear nothing, or you hear the caller and they hear silence. Signalling is fine — the SIP dialog reached in_progress — so this is always a media-plane problem, not a connection problem. (If the call never answers at all, see Call does not connect.) Work it from the symptom. Which direction is dead tells you which half of the media path to inspect first.

Which way is dead?

SymptomDead legLook here first
Caller hears the agent; you hear silenceUplink (caller → you)NAT / RTP source address — the far end can’t reach the address you advertised.
You hear the caller; caller hears silenceDownlink (you → caller)A dropped AudioBridge handle, or an SDP a=sendonly / early-media direction.
Both connect but one leg is noise, not silenceEitherCodec / payload-type mismatch on the negotiated leg.
In TeleQuick Voice, uplink and downlink are two separate media-over-QUIC tracks on the same call_sidvoice/<sid>/uplink (caller audio) and voice/<sid>/downlink (audio back to the caller). They fail independently, which is exactly why one direction can die while the other keeps working. See Sessions, calls, tracks & streams for the track model.

Cause 1 — NAT / RTP path (the classic one-way)

The most common cause. The SDP you answered with advertises the address the far end should send RTP to. Behind cloud NAT the engine binds on a private address but must advertise its public one — if the public RTP address is missing or wrong, the carrier streams caller audio into a black hole. Your egress still reaches the address they advertised, so the caller hears you and you hear nothing.
CauseFix
Trunk advertises a private RTP IP in the SDP c= line (private bind leaked into the answer).Set the trunk’s external RTP IP to the engine’s public address so the SDP carries a reachable c=. The private bind stays internal.
Asymmetric RTP: the far end sources media from a different port than it signalled, so latching never happens.The media plane latches egress to the source of the first inbound RTP packet. If no inbound packet ever arrives (see above), there’s nothing to latch to — fix the advertised address first.
A firewall or SBC in front of the trunk drops inbound RTP.Open the RTP port range to the carrier’s media IPs; confirm the SBC rewrites SDP to its own public media address.
Confirm the diagnosis with the SIP/RTP packet trace viewer: if you see RTP leaving the engine but zero inbound RTP on the caller leg, the far end is sending to an unreachable address — an advertised-IP problem, not a routing one. See SIP/RTP debugging.

Cause 2 — SDP direction attribute

An SDP media line carries a direction: sendrecv (both ways), sendonly, recvonly, or inactive. If early media (the 183 Session Progress) or the final answer negotiated anything other than sendrecv, one direction is intentionally muted by the negotiation.
CauseFix
Far end offered a=sendonly (announcement / hold-style early media) and never renegotiated to sendrecv.Expected during ringback. If it persists after answer, the caller PBX is holding the leg — check for a stray hold/re-INVITE in the trace.
Answer went out on the 183 with early media but the 200 OK SDP differs.The answer sequence must be 100183 with SDP → 200 with the same SDP. A mismatched or SDP-less 200 desyncs the media direction (and can trigger a carrier BYE).
Call is on hold — a re-INVITE flipped the leg to inactive/sendonly.Resume the leg; look for a reinvited event in the call lifecycle.

Cause 3 — Codec / payload-type mismatch

When one leg is dead as silence it’s usually a path problem (Cause 1/2). When it’s dead as noise or static, suspect the codec. The far end is streaming a payload type you didn’t answer with, so frames arrive but decode to garbage. The bridge transcodes automatically across everything it negotiated — µ-law (PCMU, PT 0), A-law (PCMA, PT 8), and Opus — including µ-law ⇄ A-law between two mismatched G.711 legs. One-way noise means the far end is sending a format that was never in the answer, so there’s nothing to transcode it to.
CauseFix
Carrier streams a payload type absent from your SDP answer.Offer both PCMU and PCMA so any PSTN carrier can pick one; let the bridge transcode to your chosen AudioCodec.
Static PT number reused for a different codec (non-standard carrier).Pin the codec on the trunk and verify the negotiated rtpmap in the trace matches what the carrier actually sends.
See the Codec guide for which codec to pick per leg and where transcoding is free.

Cause 4 — the dropped AudioBridge handle

Keep the AudioBridge handle alive. The bridge holds the live publish/subscribe on voice/<sid>/{uplink,downlink}. If you let it go out of scope, get garbage-collected, or drop it, the tracks it owns tear down — and the direction you were publishing goes silent while the other direction (served elsewhere) keeps working. This surfaces as one-way audio that appears a few seconds into a call, right when the GC runs.
If the caller stops hearing you mid-call — but the SIP dialog is still up and your onUplink callback is still firing — you almost certainly dropped the downlink publisher. Retain the bridge for the whole lifetime of the call and close it explicitly on hangup.
// WRONG — the bridge is unreferenced the moment attach() returns.
// Once GC runs, downlink stops and the caller hears silence.
await voice.audioBridge.attach(sid, {
  onUplink: (frame, tsUs) => asr.push(frame),
});

// RIGHT — hold the handle for the life of the call, close on hangup.
const bridge = await voice.audioBridge.attach(sid, {
  onUplink: (frame, tsUs) => asr.push(frame),
});
calls.set(sid, bridge);            // keep a live reference

// ...later, when the call ends:
bridge.publishDownlink(lastFrame, Date.now() * 1000);
await bridge.close();              // deterministic teardown
calls.delete(sid);
In the synchronous cores (Rust, Go) the bridge closes on drop (RAII): letting the value fall out of scope is what tears both tracks down. Bind it to a variable that lives as long as the call — e.g. let bridge = voice.audio_bridge().attach(...)?; held in your session struct — not a temporary.

Diagnose it in order

1

Confirm the dialog is actually up

Check the call reached in_progress and is still there. If it dropped, this is a connection problem, not one-way audio — go to Call does not connect.
2

Establish which direction is dead

Caller-hears-you-only ⇒ uplink dead ⇒ start at Cause 1 (NAT/RTP path). Caller-hears-silence ⇒ downlink dead ⇒ start at Cause 4 (bridge handle), then Cause 2 (SDP direction).
3

Read the packet trace

In the SIP/RTP trace viewer, look for RTP flow on each leg separately. No inbound RTP on a leg = the far end can’t reach your advertised address (Cause 1). Inbound RTP present but audio is static = codec mismatch (Cause 3). See SIP/RTP debugging.
4

Inspect the negotiated SDP

Verify the c= line carries a reachable address and the media line is sendrecv. A private c= IP or a sendonly/inactive direction is your answer.
5

Audit your bridge lifetime

If the dead direction is the one your code publishes, confirm you hold the AudioBridge handle for the whole call and only close() it on hangup.
6

Check MOS if audio is present but poor

One-way audio is binary (present or absent). If audio flows both ways but is choppy or clipped, that’s a quality problem — see MOS, jitter & loss.
Human-agent downlink (browser softphones). When you hand a call to a human agent, the human hears the caller over a media-over-QUIC track in the browser softphone. That return-audio (downlink) path is still being hardened — the caller-audio origin publish is being bridged into the relay fan-out. If a browser human agent reports one-way audio (they can talk but can’t hear the caller), validate that leg in staging; real SIP deskphone agents are not affected — their media rides the normal RTP bridge. See AI ⇄ human handoff.

SIP/RTP debugging

Read per-leg SIP and RTP traces to pinpoint the dead direction.

Codec guide

Pick a codec per leg; where transcoding is free and where mismatches bite.

Call does not connect

When the dialog never reaches in_progress in the first place.

Sessions, calls, tracks & streams

Why uplink and downlink are separate tracks that fail independently.

SIP trunking

Trunk fields for internal vs external RTP IP behind cloud NAT.

AI ⇄ human handoff

The human-agent downlink caveat and how the return-audio path is wired.