The contract is LiveKit’s own
Egma speaks LiveKit’s standard token endpoint format. That is the request every LiveKit client SDK sends throughTokenSource.endpoint,
and the answer they read back. If your frontend already has a token endpoint,
point Egma at it. LiveKit’s own example servers in Go, Node.js, Python, Ruby,
Rust, and PHP serve Egma unchanged.
Two rules are Egma’s, on top of that format. The room name always starts with
egma-sim-. The participant identity always starts with egma-persona-.
A chat simulation’s room is named egma-sim-chat-<simulation id>. It still
starts with the prefix, so your allowlist does not change. Your worker’s chat
setup reads that mark from the room name, exactly as it does when Egma mints
the token itself.
What Egma sends
OnePOST per simulation, with the auth headers you configured on the
connection.
Egma sends nothing else. No persona, no scenario, no participant metadata or
attributes. Nothing from the connection rides along beyond the agent name. The
room name is the only signal that a simulation is running.
What you can hold Egma to:
- One request per simulation. No retry. A failed request ends the simulation with the reason.
- The auth headers go to this URL only. Egma does not follow redirects. It never logs a header value, and it scrubs the values out of anything it quotes.
- HTTPS to a public host only. Egma resolves the host again at request time and refuses private addresses. It waits 20 seconds for an answer and reads at most 64 KiB of it.
- One join, over TLS, to a public server. Egma joins once, as that
identity, to that room, at the server URL you return. It holds that URL to the
rule it holds your endpoint to:
wss://orhttps://, and a host that resolves to a public address. It publishes and subscribes audio. It waits 30 seconds for your worker to join and speak, then ends the simulation asagent_never_joined. - Leave, never delete. Egma leaves when the conversation ends. It never deletes the room, and it never uses the token again.
What your endpoint returns
Any 2xx status with a JSON object. LiveKit’s format says201 Created. A
200 OK is fine.
The token must follow these rules:
- Its identity is exactly
participant_identity. The Egma SDK inside your worker addresses the persona by this identity for mock tools. - It grants join on exactly
room_name, with publish and subscribe. Leave data publishing allowed, which is the default. Mock tools use LiveKit RPC. - It carries the
room_configfrom the request as its room configuration. LiveKit reads that block only when the room is created, so do not create the room in your handler. - It expires soon. A few minutes is enough.
- It carries no admin grants. No room create, room list, or room admin.
- Check the auth header on every request, in constant time. Answer 401 or 403 otherwise.
- Refuse a
room_namethat does not start withegma-sim-. Answer 4xx. - Copy
room_configinto the token, or answer 4xx if you do not let a client name the worker. LiveKit’s own rule is 4xx for fields a client may not set. If you refuse it, your side must dispatch the worker another way: automatic dispatch, or anAgentDispatchServicecall in the same handler. - Keep a short empty timeout on your project, so the room closes after Egma leaves.
A complete handler
Python, with FastAPI andlivekit-api:
livekit-server-sdk follows the same four steps: check the header,
check the prefix, build an AccessToken for the requested identity and room,
and set at.roomConfig = RoomConfiguration.fromJson(body.room_config).
A development path with no code
LiveKit Cloud’s development token server implements the same format. Enable it on your project’s settings page and copy the token server ID. Then register a connection with:- Token endpoint:
https://cloud-api.livekit.io/api/v2/sandbox/connection-details - Auth headers:
{"X-Sandbox-ID": "<your token server id>"}
Registering the connection
In the app, choose LiveKit, voice or chat, and the connection type Token endpoint. The form asks for three things: the LiveKit agent name, the token endpoint, and the auth headers. It does not ask for your LiveKit server URL. Your endpoint answers with it. What your worker reads atctx.job.metadata
belongs to each test, as its job_dispatch_metadata, not to the connection.
Through the API, tokenEndpoint and agentName go in the config, and the auth
headers are a credential:
headersis a JSON object written inside a string. Put as many headers in it as your endpoint needs. Every value is treated as a secret.- The headers are sealed and never come back. A read shows the header names
only, as
credentialsHint. - A
urlkey is refused. The connection holds no server URL, because your endpoint’sserver_urlis where Egma connects.
egma/config.yaml:
--modality chat for a chat Connection. Neither command takes a LiveKit
server URL; the endpoint returns it for each simulation.
Hardening your endpoint
Your endpoint mints tokens into your LiveKit project. Treat it as what it is.- Require an auth header. Put a long random secret behind an
Authorizationheader, check it on every request in constant time, and configure it on the Egma connection. An unauthenticated endpoint lets anyone who learns the URL mint tokens into your project. A URL is not a secret. - Mint for exactly the identity and room that were asked for. Do not substitute your own, do not append anything, and do not mint a wildcard.
- Allowlist the
egma-sim-prefix. Refuse any otherroom_name. Then refuse the same prefix everywhere else you mint a token, so a production room can never carry a name the Egma SDK reads as a simulation. - Give the token a short expiry. It is used once, seconds after it is minted.
- Grant join, and nothing else. No room create, no room list, no admin.
- Set a short empty timeout on your project. A minute or two. This is what closes the room after Egma leaves, because Egma cannot delete it.
What closes the room
Egma leaves the room when the conversation ends. It does not delete it, on any path. Deleting a room is an administrative call signed with the project’s key pair, and Egma does not have one on this connection. A short empty timeout on your project closes the room moments after Egma’s participant leaves.When it does not work
Every one of these is a sentence Egma puts on the simulation, so the failure tells you which line to look at.- “the token endpoint at … could not be reached over HTTPS”: Egma could not open a connection. Check that the address is reachable from where Egma runs.
- “the token endpoint at … resolved to a non-public network address”: the hostname points inside a private network. Egma refuses those.
- “the token endpoint at … did not answer within 20 seconds”: your handler is too slow, or something in front of it is.
- “the token endpoint at … answered 401”, or any other non-2xx: your handler refused. Usually the auth header on the connection and the one your handler checks have drifted apart.
- “answered something that is not a JSON object”: usually a framework error page or a proxy’s HTML. The request never reached your handler, or it threw.
- “answered no token”: the body was JSON, but nothing in it was a token under
participant_tokenor one of the other accepted names. - “answered no server_url”: the body carried a token but no server. Add
server_urlto the answer. - “answered a server_url Egma cannot join”: the server URL is not
wss://orhttps://, or carries credentials. Egma sends the token over TLS only. - “answered a server_url on a non-public network address”: the server resolves inside a private network. Egma refuses it, as it refuses the endpoint itself. A self-hosted LiveKit on a private network uses project credentials instead.
- “no agent named … joined … nothing dispatched the agent”: the whole path
worked and the room stayed empty. Your endpoint did not copy
room_configinto the token, or nothing else dispatched that worker, or no worker registered under that name is running.