The gateway does not carry the voice socket, so it cannot end a call that is
already running. A voice budget admits at session start and reconciles when
the call ends. Overshoot for voice is bounded by the length of a call, not by
the size of one request.
ElevenLabs Conversational AI
Mint a signed URL for one hosted agent.X-LangWatch-Session-Id response header, so a client
that parses the vendor shape strictly can read it there instead.
Open the socket with signed_url exactly as returned. The gateway asks
ElevenLabs for the conversation id at mint time, so the call is already joined
to its spend record before the socket exists.
Closing the session
ElevenLabs reports cost and duration after the call, not over the socket. The gateway reads that report in two ways, and either one is enough:-
The post-call webhook. Point your workspace webhook at
$LANGWATCH_GATEWAY_URL/v1/convai/webhook/<model-provider-id>and store the webhook secret on the ElevenLabs provider in Settings -> Model Providers, underELEVENLABS_WEBHOOK_SECRET. The model provider id is the row id shown on that provider. Deliveries are verified against that secret; a provider with no secret stored answers 404. The URL is on the gateway, the same host as the two mints and the usage report. The gateway is built to be public, so a delivery reaches it in any topology where voice already works. Verification stays on the LangWatch app, which holds the secret; the gateway passes the raw bytes through untouched, because the signature covers them. - The reconciler. If no webhook arrives, LangWatch reads the conversation back from ElevenLabs by its own id, a couple of minutes after the mint. The webhook is faster; it is not required.
The reconciler exists because webhook delivery cannot be relied on as the only
path. A retry is not guaranteed: ElevenLabs does not retry every failed
delivery, and retries are off for HIPAA workflows. On top of that, a webhook
is disabled once it has 10 or more consecutive failures and its last
success is either older than 7 days or has never happened, which would stop
delivery for every session on it.So if the webhook were the only path, one rejected delivery would lose that
call’s billing data. It is not the only path. The reconciler asks the vendor
for the same numbers on its own schedule, so a lost delivery costs a couple of
minutes rather than a charge, and a workspace whose single webhook slot is
already used for something else still bills correctly.
post_call_transcription event is acted on. If you enable
post_call_audio or call_initiation_failure on the same webhook, LangWatch
acknowledges them and does nothing: they name the conversation but carry no
duration, and a call confirmed at zero cannot be corrected afterwards.
OpenAI Realtime
Mint an ephemeral client secret.expires_after.seconds is held inside the
range OpenAI accepts, 10 seconds to 2 hours.
Open the socket with the returned secret. Which form you use depends on where
the code runs, because a browser WebSocket takes only a URL and a
subprotocol list and cannot set request headers.
From a server, with the ws package (or any Node websocket client that
accepts headers):
session.created. The older openai-beta.realtime-v1 subprotocol is refused
by OpenAI itself with “The Realtime Beta API is no longer supported”.
Closing the session
OpenAI reports usage over the socket, inresponse.done, and that socket does
not pass through the gateway. Post what you read back:
SESSION_ID is the X-LangWatch-Session-Id from the mint. The whole
response.done event is accepted too, so you can forward the frame unchanged.
Audio tokens are taken out of the text totals before pricing, because audio
tokens cost several times what text tokens cost and charging both the total
and the audio on top would bill the audio portion twice.
A session that never reports settles as cost unknown when the settlement grace
expires, and a report arriving after that replaces the settled record.
Limiting how many calls a key runs at once
Set max open sessions on the virtual key. The request-rate limits do not bound voice: one mint is a single request that opens a call billing for as long as it runs, so a key at 60 requests per minute can hold sixty ten-minute calls without tripping anything. A mint over the cap is refused with429 realtime_session_limit, and the
refusal is recorded in the spend surface so it is visible rather than silent.
A slot frees when the call’s report arrives. A session older than the longest
call a vendor allows stops holding a slot, which matters for OpenAI, whose
socket never signals that it closed.
What the broker does not do
- It does not proxy the voice socket. No frame-level tracing, and no way to end a call in progress.
- It does not enforce the session’s tools. A hosted agent’s tools live at the vendor, and an OpenAI session declares its own.
- Guardrails do not run on a mint. The body is a session declaration, not
a prompt, and the conversation never reaches the gateway. The response says
so on
X-LangWatch-Guardrails-Not-Applied: realtime_sessionrather than reporting a check that could not mean anything.
Errors
Both mint routes serve one vendor each:
/v1/realtime/client_secrets needs an
OpenAI credential and /v1/convai/conversation/get-signed-url needs an
ElevenLabs one. A mint never falls back to another credential, because a
session credential is bound to one account and a fallback would sign for an
agent that does not exist there.
The decision behind this design is
ADR-097.