Exit
Ending a session with POST /api/session/exit: the final state, the save delta, and the upload to the callback origin.
You can trigger this from RomM's stop button, or call it directly:
curl -X POST 'http://localhost:3000/streaming/api/session/exit?slot=10' \
-H 'X-Broker-Secret: <shared secret>'This route saves a state (into the working slot, whatever slot says),
stops the emulator, dumps whatever changed in the save data, and uploads it
to the callback origin (the parent site that started the session, usually
RomM). The upload is a standard multipart form POST: POST {base_url}{BROKER_SAVE_UPLOAD_PATH} with an archive file part (a zip named
{session_id}-{timestamp}.zip) plus session_id, emulator, and, when the
rom carried them, rom_id / rom_name form fields. If activate supplied a
callback.token, it is sent as Authorization: Bearer <token>.
Query parameters:
| Parameter | Default | Purpose |
|---|---|---|
token | none | The controller token; lets the room UI exit without the secret. |
slot | 0 | The state slot to save into, -1 to 10. Mapped to the emulator's working slot the same way other routes do it; the report echoes back the slot actually used. -1 is RetroArch's auto slot and resolves the same way. |
save | true | false (or 0) stops without writing a state. The game's own save data is still flushed and shipped either way: skipping the state is the whole of "exit without saving", and discarding an in-game save the player made at a save point would mean losing real progress. |
Exit answers 403 when neither the secret nor the controller token checks
out, and 409 when there's no session to end.
Slot 0 counts as a real slot on this broker, which is why save is a
separate flag: there is no slot number left over to mean "do not save".
The callback origin
The callback base URL (where the save archive gets uploaded) is worked out from the activate request's own origin, unless the activate payload overrides it:
"callback": { "base_url": "https://romm.example.com", "token": "<upload token>" }If your broker and RomM share the same origin, you don't need to send a
base_url at all: the broker sits under the parent's SUBFOLDER, so the
origin that served the activate request already is the parent. When a reverse
proxy is involved, the broker reads that origin from the X-Forwarded-Proto
and X-Forwarded-Host headers if the proxy sets them, or from Host
otherwise.
Sending a token? Send base_url with it.
A derived origin is only as trustworthy as those headers, and
callback.token goes to whatever origin they name. If you send a token,
send an explicit base_url too, so the token can only ever go where you
meant it to. Any proxy that does sit in front of activate should set
X-Forwarded-Host itself rather than pass along one a client sent.
Report-only and failed uploads
In dev mode (BROKER_DEV_MODE=true) nothing gets uploaded: the archive is
written to BROKER_EXPORT_DIR on disk instead, and the exit report's
upload object is mode: "report-only", showing what would have been sent.
Outside dev mode, if an upload fails, the archive is still written to that
same directory so the save data isn't lost, and the report's upload object
carries mode: "failed" with the error. Either way, the export
routes can hand the archive back to you. The room chat
gets a summary message before the room is torn down.
The exit report
The response carries everything the parent needs to file the session:
| Field | Meaning |
|---|---|
status | exited |
session_id, rom | Echoed from activate. |
state_saved, state_slot, state_file | Whether a final state was saved, which slot it went into, and the written file as {path, size, mtime}. state_saved is false when you passed save=0; you can still fetch the working slot's file through the state-file GET route until the next activate. DuckStation and RPCS3 are the exception: they only serve a state the exit confirmed as saved. |
sram_flushed | Whether the game's own save data was confirmed written to disk before the emulator stopped. null when the emulator has no way to confirm it; false means the dump shipped whatever was already on disk, which may be stale, and the controller is told so in chat. |
exit_error | Only present when the emulator's own shutdown raised. The broker stops it anyway and still dumps the saves. |
save_dump | What got zipped up: files (each {path, size, mtime}, path relative to the save root), skipped and skipped_files (the count and paths of files that could not be read into the archive; a partial skip still ships the rest), total_bytes, archive_path (set only when the archive was kept on disk), and error when the dump itself failed. |
upload | The mode (uploaded, failed, report-only, or skipped when the save dump succeeded but found no save changes to upload), the target url, the HTTP status_code, and, if it failed, the error message and the archive_path the archive was written to. A failed save dump also reports mode: "failed", with no archive to keep. |
selkies_tokens_cleared | Whether selkies (the streaming layer) accepted the emptied token map. |
The route tears the session down before it answers. It tells the room the session ended, empties the token map pushed to selkies so every stream disconnects, and retires the session. The emulator object sticks around a little longer, so the state-file and screenshot GET requests still answer.