Why the phone leg is a relay
The realtime server already speaks the carrier’s codec./v1/realtime accepts
pcm16, g711_ulaw and g711_alaw, and a PSTN leg arrives as 8 kHz G.711
mu-law, base64-encoded inside a JSON envelope. The model server decodes mu-law
on the way in and re-encodes it on the way out, so the audio a carrier sends is
byte-for-byte the audio the realtime protocol wants.
Nothing needs transcoding. What differs between a carrier socket and a browser
socket is the envelope around identical bytes:
The payload string is forwarded verbatim in both directions.
start, stop,
marks and keepalives carry no audio and are dropped, and a frame whose
media.track is outbound is dropped too — that is our own audio echoed back,
and feeding it to the model makes it hear itself and interrupt its own reply.
The session opens with one session.update that pins audio/pcmu on the input
and the output, so neither side resamples audio that is already in the right
format.
That is the entire media path. It is why a phone call gets the same admission
control, the same concurrency slot, the same recording, the same transcript row
and the same per-minute billing as a browser session: there is one realtime
architecture, and the phone is a different socket on the near side of it.
The one place the relay does more than rename a field is barge-in. When the
model sees
input_audio_buffer.speech_started it abandons the reply, but the
carrier is still holding a queue of audio it was given. Only clear empties
that queue — without it the caller keeps hearing a sentence the model already
stopped saying, which reads as the agent ignoring them.Why Telnyx
The carrier leg is a fixed addend on every turn: it is latency you cannot optimize away later in the stack, so it is not a procurement decision. Telnyx measured a p95 of 118 ms on the carrier leg against Twilio’s 161 ms on independent June 2026 testing, on a network Telnyx owns end to end, at roughly half the per-minute cost. That is the whole reason it is the one carrier here. The configuration row still carries aprovider discriminator. It is the seam a
second carrier slots into, not a hint that one exists.
Connect a carrier account
You dial with your own Telnyx account; EESI does not resell numbers. Four things have to exist at Telnyx before a call connects, and Telnyx — not EESI — is where each is fixed.1
A Call Control Application
Outbound calls are placed against it and inbound calls are delivered from
it. Its id is the
connection_id you give EESI.2
An API key
Mission Control → API Keys. Keys look like
KEY….3
The Ed25519 webhook public key
Mission Control → Keys & Credentials. Every webhook is verified against it
and verification fails closed, so without the key inbound calls are never
answered and outbound calls never report how they ended — while the
configuration looks complete. This is the field people skip.
4
An Outbound Voice Profile on that application
Without one Telnyx refuses the dial even though every credential is valid.
The carrier’s own reason is surfaced verbatim on the call, so read it rather
than re-checking the key. International destinations are enabled on the same
profile.
Reads come back masked, and sending a masked value back is treated as
“unchanged” — a round trip through your own UI cannot wipe a stored key.
An organization with no telephony configuration at all falls back to the
deployment’s own carrier account, where the deployment has one configured. The
moment the organization has a configuration row of its own, its credentials are
used instead — and a set of rows with none of them marked default outbound is an
error rather than a fallback, because silently switching the caller ID is worse
than refusing to dial.
Add a number
Numbers hang off the configuration rather than living inside it, because a number can be a caller ID, an inbound number, or both, and none of that is a credential.addressis E.164, or bare digits plus acountry_codeso the canonical form can be derived. It is immutable — to change a number, delete the row and create another.inbound_enabledis what makes EESI bind the number at the carrier: the number’sconnection_idis set to your Call Control Application, which is how one platform-wide webhook serves every number without a URL per number. The bind runs when you create the number with inbound on, and again the first time you switch it on — a label edit does not pay for a carrier round trip.- The response carries
provider_sync.ok: falsemeans the row was saved but the carrier-side bind failed, so the number will not ring through until it is retried. A 200 is not the whole story. inbound_instructionsis the persona for calls to this number. Left unset, the session opens with the model’s default.
409
rather than becoming a call nobody can route.
Outbound
Outbound calls are placed by Care today: its dispatcher resolves the organization’s carrier credentials, stages a session brief, mints a capability and posts to Telnyx’s/calls. The /v1/phone router itself has no
“place a call” endpoint — it serves the media socket and the two webhooks.
A dial carries the media URL with it:
Handing the stream URL over at dial time, rather than in a later answer webhook,
means the media socket is already addressed when the person picks up — one fewer
round trip between “hello” and the model hearing it.
The capability on the media socket
The carrier connects to/v1/phone/media seconds later with no API key, no
cookie and no session of any kind. The URL is therefore the credential, and it
has to be one that cannot be guessed, cannot be replayed tomorrow, and says
exactly which call it is for:
- Signed with a key derived from the deployment’s secret-encryption key by HKDF under its own label, so a leaked capability key unwraps no stored secrets.
- Valid for 15 minutes. The carrier dials back within seconds, so the window exists only to make a URL captured from a carrier-side log worthless by the time anyone reads it.
- Bound to one organization and one call. A valid capability for one call is not a valid capability for another.
Past the handshake, the leg is an ordinary realtime session: it holds a
concurrency slot, opens a speech-session row with
source: "phone", always
records a transcript, and debits realtime seconds from the moment the upstream
pipeline connects, so a call that rings out costs nothing.
The session brief
The platform places calls and carries audio; it does not know what a call is about. Whichever product originates a call writes a brief to Redis underphone:brief:{call_uuid} before it dials, and the media socket reads it when the
carrier connects.
The greeting matters more than it looks. Nobody who answers a phone speaks until
spoken to, so a call that waits for speech waits forever.
The brief expires on its own after 15 minutes, so an abandoned dial leaves
nothing to sweep. A brief that cannot be read is survivable — the session
opens with the model’s default persona, which is a worse call rather than a
failed one. A brief that cannot be written is the product’s call to make:
Care refuses to dial at all, because a companion call in a generic persona is
the wrong call to place.
Inbound
POST /v1/phone/inbound is one webhook for every number on the platform. The
payload names both the connection and the dialled number, which is enough to
resolve the organization — that is what binding a number to the Call Control
Application buys, and why there is no per-number URL to keep in sync.
The ordering is the part worth reading, because the key a webhook is verified
with belongs to an organization, and which organization is a fact inside the
unverified payload:
1
Filter
Anything that is not
call.initiated with direction: "incoming" is
acknowledged and dropped — including our own outbound legs, echoed back on
the same connection.2
Resolve, without acting
connection_id and to are read as untrusted data and resolved to a
configuration, a number row, and the stored public key. A keyed read on
non-secret identifiers that changes nothing.3
Verify
Ed25519 over
{telnyx-timestamp}|{raw body} — the exact bytes as received,
since re-serializing the parsed body changes whitespace and key order and
fails every check. A timestamp more than 300 seconds from ours is refused,
so a captured webhook does not replay forever.4
Answer
The number has to have
inbound_enabled, and the organization has to have
credit — an account out of credit does not get its phone answered, which is
quieter than answering and cutting the caller off mid-sentence. Then a
capability is minted, inbound_instructions becomes the brief, and Telnyx
is told to answer with the same media URL an outbound call gets.204, identically. A carrier retries anything that is not
a 2xx, so an event we chose not to act on still has to be acknowledged — and an
unauthenticated caller must not be able to learn from the responses which
numbers exist, which of them answer, or which signature was the wrong one.
From the websocket onward there is no difference between a call the platform
placed and one it received.
Call events
POST /v1/phone/events is the outbound webhook. Two facts matter and neither is
visible from inside the media socket:
A voicemail greeting transcribes as speech, so without the first of these a
machine would be graded as a conversation that happened.
The public key an event is verified against is resolved from the call row, not
from the webhook, so a forged event cannot nominate the key it wants to be
checked with. Events for calls the platform does not know are acknowledged and
dropped.
Going further
EESI Care
The product that places outbound calls, and what it does with the
transcript.
Realtime
The protocol on the far side of the relay.
Sessions
Transcripts, recordings and reply latency for every call.
Compliance
Disclosure, consent and recording duties that attach to calling people.