One carrier, Telnyx, and a relay rather than a pipeline. The whole telephony implementation is a media translator, a carrier client, a signed capability and two webhooks — because the interesting work turned out to be work nobody needed to do.

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 a provider 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.
Then create the configuration. Telephony configuration is not in the SDK, so call it directly:
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.
  • address is E.164, or bare digits plus a country_code so the canonical form can be derived. It is immutable — to change a number, delete the row and create another.
  • inbound_enabled is what makes EESI bind the number at the carrier: the number’s connection_id is 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: false means 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_instructions is the persona for calls to this number. Left unset, the session opens with the model’s default.
The tuple of (provider, connection id, normalized address) has to be globally unique — inbound dispatch keys on it without the organization, so the same number registered twice against the same Telnyx account is refused with a 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.
A signed capability rather than a database row because the check happens on the hot path of somebody picking up a phone, and a signature needs no round trip. Refusals close the socket before the handshake completes, with a reason: 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 under phone: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.
Every refusal answers 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.