webstation-broker
REST API

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:

ParameterDefaultPurpose
tokennoneThe controller token; lets the room UI exit without the secret.
slot0The 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.
savetruefalse (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:

FieldMeaning
statusexited
session_id, romEchoed from activate.
state_saved, state_slot, state_fileWhether 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_flushedWhether 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_errorOnly present when the emulator's own shutdown raised. The broker stops it anyway and still dumps the saves.
save_dumpWhat 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.
uploadThe 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_clearedWhether 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.

On this page