A user finishes a text chat on the homepage. Clicks the call button. The voice agent answers. Somewhere between the click and the hello, a session ID needs to ride along — from browser, through ElevenLabs, into a Custom LLM proxy I control.
Where does it live on the way?
The obvious answer is metadata. Every SDK has a metadata field. That is what metadata is for. I put the UUID there. The proxy logged the request. Metadata: empty. ElevenLabs accepted the field. Nothing downstream ever saw it.
Next: user_id. A first-class identity field, plumbed end-to-end in most stacks. I put the UUID there. The proxy logged the request. The user_id field was present and held a value ElevenLabs had generated for itself. Mine was gone.
Next: custom parameters. The SDK accepts a loose bag of key-value pairs at session start. I passed the UUID. The session refused to start. No error worth reading. Just dead air where a voice should be.
Three doors. Three failures. One quiet, one overwritten, one locked.
The door that opened was the one I had been treating as furniture.
The agent has a system prompt. The system prompt is a template. The template can hold placeholders. The SDK accepts a dynamicVariables object at startSession. ElevenLabs substitutes the values into the template before sending it to the LLM. The LLM receives a system message with my UUID inlined into the English.
The proxy reads messages[0]. Regex out the UUID. Correlate the call.
It works because the system prompt is the one field the model is contractually required to receive. Metadata can be dropped. user_id can be rewritten. Custom params can be rejected. The system prompt is load-bearing. If it doesn't arrive, there is no agent.
So the prompt becomes two things at once. Instructions to the model. Envelope for data the model was never meant to read. The model doesn't know the UUID is for me, not it. The model glances at the string, finds no instruction in it, moves on. The proxy, watching the same message go by, lifts the UUID off it like a passenger off a train.
This is not documented. It is not clean. It treats a natural-language field as a transport layer.
It is also the only thing that works.
The lesson is older than the stack. In a multi-vendor pipeline, the channels that look like channels — labeled, typed, documented — are the channels the vendors feel free to break. The channel nobody can break is the one the product itself depends on. Hide your signal there.
Where does a session ID actually live in a multi-vendor agent stack?
In the only field the model is forced to read.
References: the ElevenLabs Conversational AI dynamic variables docs, which describe the substitution mechanism without ever suggesting it as a transport for opaque session state.
Claude Shannon defined a channel as anything that carries a signal from sender to receiver. He did not specify that the channel had to be labeled as one. The engineers who built this stack drew the channels on the diagram. The signal found a different path.