Users and invites
Adding people to a running session with POST /api/session/join, and minting invite links.
Add a user to the session
curl -X POST http://localhost:3000/streaming/api/session/join \
-H 'X-Broker-Secret: <shared secret>' \
-H 'Content-Type: application/json' \
-d '{
"user": { "id": 2, "username": "player2", "display_name": "Player Two" },
"permission": "participant"
}'Returns {"status": "joined", "url": "/streaming/?token=<personal token>", ...},
409 when no session is up, 422 for a permission other than participant
or readonly, and 429 when the room already holds
BROKER_MAX_ROOM_VIEWERS people.
permission: "readonly" for spectators. Re-joining
the same user (by id, else username) replaces their old token.
Every user gets a personal token. The broker pushes the full token map,
{token: {role, slot, mk_control}}, to selkies, which enforces input routing
per streaming connection: a participant can be handed a gamepad slot or the
mouse and keyboard from the room UI by dragging the icons onto them; a
spectator never gets input. Removing a user from the room revokes the token and
selkies drops that connection live.
Invite links
curl -X POST 'http://localhost:3000/streaming/api/session/invite?token=<controller token>' \
-H 'Content-Type: application/json' \
-d '{ "permission": "participant" }'This route is gated by the controller token instead of the broker secret,
since the room UI (which never holds the broker secret) is what actually
calls it. It answers 403 without a valid controller token, and 422 for a
permission other than participant or readonly.
Returns the session's shareable link for that permission (?invite=...
rather than ?token=...). It is the same link on every call, seats nobody by
itself, and can go to any number of people: each person who opens it is given
their own seat by GET /api/session/context and joins under the name they
pick in the room. The invite route ignores the session's multiplayer flag:
a link always works. That flag only governs whether RomM advertises the
session for joining and whether the room shows its comms surface (chat,
webcam) while the host is alone.
Seats live for the whole session. Disconnecting (closing the tab, reloading, a network blip) releases any gamepad slot or mouse/keyboard the seat was holding but keeps the seat, and the room remembers its seat per tab, so a reload lands back in the same seat rather than taking another. Both kinds of link stop working when the session ends.
The room
The URL activate and join return is the collab room, a vanilla JS page that
iframes the selkies stream and connects to the broker's websocket at
<SUBFOLDER>/ws/room. It bootstraps from GET /api/session/context?token=...,
which tells it who the token belongs to and what it may do. The roster of
who's in the room isn't part of that response. It arrives later, over the
websocket's state_update broadcast. Over the websocket the room carries
chat, webcam and microphone fanout, resolution changes, and gamepad and
mouse/keyboard reassignment. The wire format is documented on the
room module in the Python reference.
Emulator menu
curl -k -X POST 'https://localhost:3001/streaming/api/session/menu?token=<controller token>'Opens or closes the running emulator's own menu (RetroArch's Quick Menu),
for players with no keyboard or gamepad combo to do it themselves. Like the
exit route it takes the controller token, or the broker secret in
X-Broker-Secret. A participant or read-only token answers 403. The
context response's emulator names the running emulator; the room only
shows its button to the controller when it's retroarch.
Returns {"status": "ok"}. You get 400 if the emulator has no menu, 409
if there's no active session, the emulator isn't running, or another session
action is in progress, and 502 if the emulator refused the toggle. A
toggle is also refused while a save, a resume load or a disc swap is
running, so it can't land in the middle of one.