Device Sync Protocol
This is a reference for the protocol RomM uses for bidirectional sync with companion apps. End-user view in Saves & States, with operator-side SSH transport in SSH Sync.
Primitives
- Device: a registered endpoint, bound to a Client API Token.
- Sync session: one atomic bidirectional run (pull, push, conflict reconcile, play-session ingest).
- Play session: per-ROM playtime record, posted standalone or batched at sync end.
Authentication
Every call: Authorization: Bearer rmm_.... Required scopes:
| Endpoint family | Scope |
|---|---|
/devices/* |
devices.read, devices.write |
/sync/* |
assets.read, assets.write, devices.write |
/play-sessions/* |
me.read, me.write (read own), users.read (read others') |
/assets/* (save/state I/O) |
assets.read, assets.write |
Registering a device
After pairing:
POST /api/devices
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "RG35XX - Living Room",
"platform": "muos",
"hostname": "rg35xx-livingroom.local",
"mac": "aa:bb:cc:dd:ee:ff",
"sync_mode": "push_pull",
"paths": { "roms": "/roms", "saves": "/saves", "states": "/saves/states" }
}
Response includes id, which the device caches for subsequent calls. sync_mode can be pull_only (server → device), push_only (device → server), or push_pull (bidirectional, default).
Sync negotiation
The device sends what it has and RomM returns what to do:
POST /api/sync/negotiate
{
"device_id": 17,
"roms": [
{
"rom_id": 1234,
"saves": [
{ "file": "mario.srm", "mtime": "2026-04-18T09:42:01Z", "sha1": "abc..." },
{ "file": "mario.state", "mtime": "2026-04-18T09:45:00Z", "sha1": "def..." }
]
}
]
}
Response is a list of operations:
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"operations": [
{
"type": "upload",
"rom_id": 1234,
"file": "mario.srm",
"destination": "/api/saves"
},
{
"type": "download",
"rom_id": 5678,
"file": "zelda.srm",
"source": "/api/saves/42/content",
"dest_path": "/saves/zelda.srm"
},
{
"type": "conflict",
"rom_id": 9999,
"file": "tetris.srm",
"resolution": "keep_both"
},
{ "type": "noop", "rom_id": 1111, "file": "goldeneye.srm" }
]
}
| Op | Meaning |
|---|---|
upload |
Device POSTs the file to destination. |
download |
Device GETs source and writes to dest_path. |
conflict |
Both sides newer, resolution must be one of keep_both (default), server_wins, device_wins. |
noop |
Hashes match, nothing to do. |
Upload (POST /api/saves, multipart) and download (GET /api/saves/{id}/content) both require the bearer token.
Completing a session
POST /api/sync/sessions/{session_id}/complete
{
"operations_completed": 15,
"operations_failed": 1,
"play_sessions": [
{ "rom_id": 1234, "start": "2026-04-18T09:00:00Z", "end": "2026-04-18T09:45:00Z", "duration_seconds": 2700 }
]
}
Closes the session and ingests batched play sessions in one call.
Rate limits and polling
- Sync once per session, not per save
- Don't poll
/api/sync/negotiatetightly - No push channel yet, so polling is the only model
See also
- Client API Tokens: auth and pairing
- API Authentication: general auth primer
- API Reference: full endpoint catalogue
- SSH Sync: alternative transport
- Argosy, Grout: reference client implementations