Methods
Methods are used to execute actions and request data back from the API.
Access
Each method below identifies which clients may call it:
- Unauthenticated bootstrap: JSON-RPC methods
settings.auth.claim,settings.auth.status,settings.auth.link, and redactedsettings.auth.link.statusare available before authentication. Remote HTTP POST requiresallowed_ipsand remains rate limited. Separate client-pairing endpoints/api/pair/startand/api/pair/finishare remotely reachable and strictly rate limited./healthis unrestricted and returns a small JSON body carrying the service's coarse lifecycle state (starting,readyorfailed) and nothing else. - All accepted clients: localhost, authenticated admin, authenticated member, and legacy clients admitted by platform compatibility policy.
- Localhost or any authenticated client: localhost, paired clients, and API-key admin. Legacy clients are rejected.
profiles.manage: localhost and clients with the capability. Admin has it; member does not. Legacy retains it only on approved appliance platforms.settings.write: localhost and clients with the capability. Admin has it; member does not. Legacy retains it only on approved appliance platforms.input: localhost, member, and admin. Legacy input is grandfathered only on MiSTer, MiSTeX, Batocera, and ReplayOS.screenshot: localhost, member, and admin. Legacy screenshot capture is grandfathered only on MiSTer and ReplayOS.update.apply: localhost and authenticated admin. Member and legacy do not receive it.- Localhost or admin: localhost, paired admin, and API-key admin. Member and legacy are rejected.
- Localhost only: requests originating from Core's device. All remote clients are rejected.
Use clients.current to inspect current connection's access state, paired role, and effective capabilities. A method may also require a resource-specific credential, such as a profile PIN; those requirements are documented separately from connection access.
Launching
run
Access: All clients.
Emulate the scanning of a token. Access is decided when the API accepts run; mapped and expanded ZapScript commands do not re-evaluate connection capabilities during internal execution. Legacy run access therefore remains limited to explicitly grandfathered platforms.
Parameters
Accepts two types of parameters:
- A string, in which case the string will be treated as the token text with all other options set as default.
- An object:
| Key | Type | Required | Description |
|---|---|---|---|
| type | string | No | An internal category of the type of token being scanned. Not currently in use outside of logging. |
| uid | string | No* | The UID of the token being scanned. For example, the UID of an NFC tag. Used for matching mappings. |
| text | string | No* | The main text to be processed from a scan, should contain ZapScript. At most 8192 bytes. |
| data | string | No* | The raw data read from a token, converted to a hexadecimal string. Used in mappings and detection of NFC toys. |
| unsafe | boolean | No | Allow unsafe operations. Default is false. |
These parameters allow emulating a token exactly as it would be read directly from an attached reader on the server. A request's parameters must contain at least a populated uid, text or data value.
Result
Returns null once the ZapScript has finished executing without error. The method waits for execution to complete: mapping, parsing, launch policy (profiles, playtime limits, blocked commands, hooks), media lookup and every command in the script. Success means the script ran to completion; it does not prove the launched software is still running afterwards.
If execution fails, the response carries an error whose data.category is one of:
| Category | Meaning |
|---|---|
busy | Another launch is already in progress. |
media_not_found | The requested media could not be found or matched. |
disabled | ZapScript execution is disabled in settings. |
invalid_script | The script could not be parsed, names an unknown command or system, or exceeds 8192 bytes. |
blocked | Execution was refused by configuration, a profile requirement or a hook. |
playtime_limit | A playtime limit prevented the launch. |
timeout | Core stopped waiting after the request timeout (30 seconds). Anything already started continues. |
cancelled | The request was cancelled, for example because the connection closed. Anything already started continues. |
unavailable | The service is shutting down. |
launch_repair | The launch needs the user to fix something first. See launch repair errors. |
execution_failed | Any other execution failure. |
Error messages are fixed per category and never include filesystem paths or token contents; the details are in the Core log. timeout and cancelled only mean Core stopped waiting: nothing that already started is rolled back. During shutdown the connection often closes before the unavailable response can be written, so treat a dropped connection with a request in flight the same way.
Launch repair errors
A launch_repair error means the launch stopped for something the user can act on, such as an uninstalled launcher or a missing permission. Its data carries two extra keys that let a client write and localize its own wording:
| Key | Type | Required | Description |
|---|---|---|---|
reason | string | Yes | A machine readable reason from the closed set below. |
params | object | No | Display names for the reason's wording. Present only when the reason carries one. |
The reasons and the parameters each one can carry:
| Reason | Meaning | Parameters |
|---|---|---|
launcher_not_installed | The launcher application is not installed. | launcher, plugin |
launcher_component_missing | The launcher is installed, but the entry point it declares is gone or disabled. | launcher, plugin |
launcher_plugin_missing | The launcher is installed but its plugin or core for this system is absent. | launcher, plugin |
launcher_version_unsupported | The installed build of the launcher cannot be used for this media, for example because its storage model is unsupported. The user needs a different build of that launcher. | launcher, plugin |
launcher_ambiguous | Several usable launchers and no reviewed default; the user must choose one. Reserved: see below. | launcher, plugin |
launcher_unsupported_media | This launcher cannot play the selected media entry. | launcher, plugin |
launcher_options_unsupported | The launch options requested are not supported by this launcher. | launcher, plugin |
storage_permission_required | The launcher lacks the storage permission it needs. | launcher, plugin |
storage_provider_unsupported | The media lives on a provider this launcher cannot read. | launcher, plugin |
storage_unavailable | The storage holding the media is not present. | launcher, plugin |
media_unavailable | The media file cannot be resolved or opened. | launcher, plugin |
media_access_revoked | The host's own access to this media's source was revoked or withdrawn, distinct from media_unavailable's unspecified cause: the user can act on this one by re-granting access. | launcher, plugin |
host_unavailable | The host's launch service is not answering. | launcher, plugin |
host_foreground_required | The launch needs the user to return to the app first. | launcher, plugin |
cancelled | The launch was cancelled before it started. | launcher, plugin |
outcome_unknown | The launch was dispatched, but the result could not be confirmed. | launcher, plugin |
refused | The operating system refused the request. This is not a catch-all. | launcher, plugin |
unspecified | Core sent no structured reason. Show message verbatim. | launcher, plugin |
refused means specifically that the operating system refused the launch. A failure Core cannot classify that far reports unspecified instead, so do not treat refused as "something else went wrong".
launcher_ambiguous is reserved. No Core version emits it yet, because launcher selection resolves by catalog precedence rather than asking the user. It is published so that clients can handle it when a version does; do not wait for it.
params has a closed key set: launcher is the launcher application's display name, such as RetroArch or DuckStation, and plugin is the name of its plugin or core for this system, such as Mesen. Both are short display names only, never identifiers, paths, URIs or text from the host. A key is absent when Core has no name for it, so treat both as optional for every reason.
Clients must tolerate an unknown reason, and an absent one from an older Core, by falling back to the error's message, exactly as they do for unspecified. The message is a fixed English string that reads sensibly on its own, so it is always a usable last resort, but it is not a stable contract: branch on reason wherever the wording matters.
Physical reader scans, playlists and the launch endpoint are not affected. They remain asynchronous and do not report execution failures.
For ZapScript launch.random, Core selects uniformly from matching non-missing media rows after applying systems, tags, and path scope. Filesystem and virtual path targets recursively include subfolders. Tagged requests never use filesystem fallback because unindexed files have no tag metadata.
Compatibility
Earlier Core versions returned null as soon as the token was accepted, before execution started, and never reported execution failures. Clients that treated an immediate null as "launched" should now expect the response to arrive when execution finishes, or when Core stops waiting for it. A timeout or cancelled error means Core stopped waiting, not that execution ended: work already started continues, so poll for the effect rather than treating either as proof of failure. Any other error response is authoritative.
Aliases
launch is a deprecated alias for run with identical parameters and result. run.script is reserved and currently returns a method-not-found error.
Example
Request
{
"jsonrpc": "2.0",
"id": "52f6242e-7a5a-11ef-bf93-020304050607",
"method": "run",
"params": {
"text": "**launch.system:snes"
}
}
Response
{
"jsonrpc": "2.0",
"id": "52f6242e-7a5a-11ef-bf93-020304050607",
"result": null
}
Error response
{
"jsonrpc": "2.0",
"id": "52f6242e-7a5a-11ef-bf93-020304050607",
"error": {
"code": 1,
"message": "media not found",
"data": {
"category": "media_not_found"
}
}
}
Launch repair error response
{
"jsonrpc": "2.0",
"id": "52f6242e-7a5a-11ef-bf93-020304050607",
"error": {
"code": 1,
"message": "this launcher's plugin for this system is not installed",
"data": {
"category": "launch_repair",
"reason": "launcher_plugin_missing",
"params": {
"launcher": "RetroArch",
"plugin": "Mesen"
}
}
}
}
stop
Access: All clients.
Kill any active launcher, if possible.
This method is highly dependant on the platform and specific launcher used. It's not guaranteed that a launcher is capable of killing the playing process.
Parameters
None.
Result
Returns null on success, meaning nothing was running or the media was
confirmed stopped. Active media is only cleared when the stop succeeded.
Returns an error when the media is known to still be running, such as a launcher process that ignored both a close request and a forced kill. Launchers that never expose a process at all, like an opened browser tab, still report success because there is nothing to stop.
Whether a failure can be detected at all is platform-dependent. Windows reports one when it can confirm the media is still running; platforms that cannot confirm it continue to report success.
Example
Request
{
"jsonrpc": "2.0",
"id": "176b4558-7a5b-11ef-b318-020304050607",
"method": "stop"
}
Response
{
"jsonrpc": "2.0",
"id": "176b4558-7a5b-11ef-b318-020304050607",
"result": null
}
confirm
Access: All clients.
Confirm and launch a staged token from the launch guard.
When launch guard is enabled and media is playing, scanned tokens are staged instead of launched immediately. This method confirms the currently staged token and launches it.
Parameters
None.
Result
Returns null on success. Returns an error if no token is currently staged.
Example
Request
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5b-11ef-b318-020304050607",
"method": "confirm"
}
Response
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5b-11ef-b318-020304050607",
"result": null
}
UI
Core exposes transient UI requests so connected clients—and host platform when appropriate—can render same notice, loader, picker, or confirmation in parallel. Core initially keeps at most one active request, but API uses arrays for future expansion. First valid response for event ID wins; stale responses fail.
UI events are intended for small, non-sensitive interactions. They are broadcast to every permitted connected client. Never use them for PINs, passwords, recovery codes, or other secrets.
ui
Access: All clients.
Returns authoritative UI event state. Clients should call this after connecting or reconnecting.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| revision | number | Yes | Monotonic revision of global UI state shared across clients. Ignore older snapshots. |
| events | UI event[] | Yes | Active events. Initial implementation contains zero or one event. |
| resolved | UI resolution[] | Yes | Always empty in query response; terminal resolutions are delivered by ui.changed. |
UI event object
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Opaque event ID required by ui.respond. |
| kind | string | Yes | notice, loader, picker, or confirm. |
| title | string | No | Optional heading. |
| message | string | No | Optional body text. |
| choices | object[] | No | Picker choices containing opaque id and display label. |
| selectedChoiceId | string | No | Initially selected picker choice. |
| dismissible | boolean | Yes | Whether dismiss is accepted. |
| createdAt | string | Yes | RFC3339 creation timestamp. |
| expiresAt | string | No | Authoritative RFC3339 expiry. Omitted for producer-controlled events such as loaders. |
Choice IDs are presentation-safe. Executable ZapScript and private choice values remain inside Core.
Example
{
"jsonrpc": "2.0",
"id": "ui-state-1",
"method": "ui"
}
{
"jsonrpc": "2.0",
"id": "ui-state-1",
"result": {
"revision": 8,
"events": [
{
"id": "56969e9c-f863-4cc8-9c2c-d7512bf10d4d",
"kind": "confirm",
"title": "Change game?",
"message": "**launch.system:snes",
"dismissible": true,
"createdAt": "2026-07-16T12:00:00Z",
"expiresAt": "2026-07-16T12:00:15Z"
}
],
"resolved": []
}
}
ui.respond
Access: All clients.
Responds to active UI event. First valid response wins globally and closes host/client renderers.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Active event ID. |
| action | string | Yes | dismiss, select, or confirm. |
| choiceId | string | No | Required for picker select; must identify one published choice. |
Allowed actions:
notice:dismisswhen dismissibleloader:dismissonly when explicitly dismissiblepicker:selectwithchoiceId, ordismissconfirm:confirm, ordismisswhen dismissible
Returns null when accepted. Returns client error for stale event ID, invalid action, missing/unknown choice, expired event, or non-dismissible event.
Example
{
"jsonrpc": "2.0",
"id": "ui-response-1",
"method": "ui.respond",
"params": {
"id": "56969e9c-f863-4cc8-9c2c-d7512bf10d4d",
"action": "confirm"
}
}
{
"jsonrpc": "2.0",
"id": "ui-response-1",
"result": null
}
Top-level confirm remains launch-guard-specific for compatibility. It cannot confirm unrelated generic UI event.
UI resolution object
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Resolved event ID. |
| outcome | string | Yes | confirmed, selected, dismissed, timed_out, completed, superseded, or cancelled. |
| choiceId | string | No | Selected opaque choice ID for selected. |
Tokens
tokens
Access: All clients.
Returns information about active and last scanned tokens.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| active | TokenResponse[] | Yes | A list of currently active tokens. |
| last | TokenResponse | No | The last scanned token. Null if no token has been scanned yet. |
Token object
| Key | Type | Required | Description |
|---|---|---|---|
| type | string | Yes | Type of token. |
| uid | string | Yes | UID of the token. |
| text | string | Yes | Text content of the token. |
| data | string | Yes | Raw data of the token as hexadecimal string. |
| scanTime | string | Yes | Timestamp of when the token was scanned in RFC3339 format. |
| readerId | string | No | ID of the reader that scanned the token. |
Example
Request
{
"jsonrpc": "2.0",
"id": "5e9f3a0e-7a5b-11ef-8084-020304050607",
"method": "tokens"
}
Response
{
"jsonrpc": "2.0",
"id": "5e9f3a0e-7a5b-11ef-8084-020304050607",
"result": {
"active": [],
"last": {
"type": "",
"uid": "",
"text": "**launch.system:snes",
"data": "",
"scanTime": "2024-09-24T17:49:42.938167429+08:00"
}
}
}
tokens.history
Access: All clients.
Returns a list of the last recorded token launches.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| entries | LaunchEntry[] | Yes | A list of recorded token launches. |
Launch entry object
| Key | Type | Required | Description |
|---|---|---|---|
| data | string | Yes | Raw data of the token as hexadecimal string. |
| success | boolean | Yes | True if the launch was successful. |
| text | string | Yes | Text content of the token. |
| time | string | Yes | Timestamp of the launch time in RFC3339 format. |
| type | string | Yes | Type of token. |
| uid | string | Yes | UID of the token. |
Example
Request
{
"jsonrpc": "2.0",
"id": "5e9f3a0e-7a5b-11ef-8084-020304050607",
"method": "tokens.history"
}
Response
{
"jsonrpc": "2.0",
"id": "5e9f3a0e-7a5b-11ef-8084-020304050607",
"result": {
"entries": [
{
"data": "",
"success": true,
"text": "**launch.system:snes",
"time": "2024-09-24T17:49:42.938167429+08:00",
"type": "",
"uid": ""
}
]
}
}
Media
media
Access: All clients.
Returns the current media database status and active media.
The database status includes both indexing and optimization information:
- Indexing takes priority over optimization in the response (if both are running, only indexing status is shown)
- Optimization status and progress are shown when no indexing is in progress
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| database | IndexingStatus | Yes | Status of the media database. |
| active | ActiveMedia[] | Yes | List of currently active media. |
| playlists | PlaylistState[] | No | Currently active playlist slots. |
Indexing status object
| Key | Type | Required | Description |
|---|---|---|---|
| exists | boolean | Yes | True if the database exists. |
| indexing | boolean | Yes | True if indexing is currently in progress. |
| optimizing | boolean | Yes | True if database optimization is currently in progress. |
| totalSteps | number | No | Total number of indexing steps. |
| currentStep | number | No | Current indexing step. |
| currentStepDisplay | string | No | Display name of the current indexing step or optimization step. |
| totalFiles | number | No | Total number of files to index. |
| totalMedia | number | No | Total number of media entries in the database. Only included when database exists and is not indexing. |
| scan | object | No | The current system's folder scan, only included while a scan is running: path is the folder being read and entries the files and folders read so far. |
Active media object
| Key | Type | Required | Description |
|---|---|---|---|
| mediaId | number | No | Opaque media database row ID for efficient follow-up media.meta and media.image requests. Omitted when the active path cannot be resolved in the current media database. |
| launcherId | string | Yes | ID of the launcher. |
| systemId | string | Yes | ID of the system. |
| systemName | string | Yes | Display name of the system. |
| mediaPath | string | Yes | Path to the media file. |
| relativePath | string | No | Launcher-relative convenience path, when it can be derived. Not a stable media identity. |
| positionMs | number | No | Current playback position in milliseconds when reported by the launcher. Currently available for native audio. |
| durationMs | number | No | Total playback duration in milliseconds when reported by the launcher. Currently available for native audio. |
| playbackState | string | No | Launcher-reported playback state: playing, paused, or stopped. Currently available for native audio; omitted when unavailable. |
| mediaName | string | Yes | Display name of the media. |
| slot | string | No | Media slot for the item. Omitted or primary is foreground media; background is background audio. |
| started | string | Yes | Timestamp when media started in RFC3339 format. |
| zapScript | string | Yes | ZapScript command to launch this media item. |
| launcherControls | string[] | No | List of control action names supported by the active launcher. Only present if the launcher supports controls. See media.control. |
Playlist state object
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Playlist ID. |
| name | string | Yes | Playlist display name. |
| slot | string | Yes | Playlist slot, primary or background. |
| repeat | string | Yes | Repeat mode: none, all, or one. |
| items | object[] | Yes | Playlist items. |
| index | number | Yes | Zero-based current item index. |
| total | number | Yes | Total item count. |
| playing | boolean | Yes | Whether playlist slot is playing. |
| unsafe | boolean | Yes | Read-only. True when the playlist's items came from a source this device does not trust, such as a fetched ZapLink or a deck shared by someone else, and also when an untrusted script opened it, which even the user's own deck inherits. Commands in those items that drive input or run programs will not run. Core versions that do not report this field do not restrict playlist items, so treat a missing value as false. |
Example
Request
{
"jsonrpc": "2.0",
"id": "47f80537-7a5d-11ef-9c7b-020304050607",
"method": "media"
}
Response (database ready)
{
"jsonrpc": "2.0",
"id": "47f80537-7a5d-11ef-9c7b-020304050607",
"result": {
"database": {
"exists": true,
"indexing": false,
"optimizing": false,
"totalMedia": 1337
},
"active": []
}
}
Response (optimization in progress)
{
"jsonrpc": "2.0",
"id": "47f80537-7a5d-11ef-9c7b-020304050607",
"result": {
"database": {
"exists": true,
"indexing": false,
"optimizing": true,
"currentStepDisplay": "vacuum",
"totalMedia": 1337
},
"active": []
}
}
media.search
Access: All clients.
Query the media database and return matching indexed media. Hidden entries are excluded before pagination unless includeHidden is true. Explicit required user tag filters (user:favorite, user:liked, user:hidden and the other user tags) also include hidden entries; OR/NOT user tag filters do not enable this exception.
Note: This API uses cursor-based pagination for all requests. The total field is deprecated and returns only the current response-page count; it is not the full match count. Use the pagination object to navigate through results. For subsequent pages, include the nextCursor value and repeat the same systems, pathPrefix, query, tags, letter, and sort scope. Changing includeHidden or editing media preferences invalidates existing search cursors; restart without a cursor when Core reports library visibility changed.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| query | string | No | Case-insensitive search by filename. By default, query is split by white space and results are found which contain every word. If omitted, all media is returned. |
| systems | string[] | No | Case-sensitive list of system IDs to restrict search to. A missing key or empty list will search all systems. |
| includeHidden | boolean | No | Include hidden entries for recovery/management. Defaults to false; available to all clients. Repeat on subsequent pages. |
| pathPrefix | string | No | Recursively restrict results beneath a filesystem directory or virtual route. Matching respects path boundaries, so /roms/SNES does not include /roms/SNES2; % and _ are literal path characters. |
| maxResults | number | No | Max number of results to return. Default is 100. |
| cursor | string | No | Cursor for pagination. Omit for first page, use nextCursor from previous response for subsequent pages with the same scope and sort. |
| tags | string[] | No | Filter results by case-sensitive tags. Maximum 50 tags, each up to 128 characters. Default and + filters require matches, - excludes matches, and ~ joins alternatives. Can be used without query or systems for tag-only searches. |
| letter | string | No | Filter results by first character of game name. Supports: A-Z (single letters), "0-9" (numbers), "#" (symbols). Case-insensitive. |
| sort | string | No | Explicit order: name-asc, name-desc, filename-asc, or filename-desc. Name uses the returned display name with SQLite's case-insensitive collation; filename uses full indexed path. Omitted preserves legacy database order. |
| fuzzySystem | boolean | No | Enable fuzzy matching for system IDs in the systems array (e.g., "snes" matches "SNES"). |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| results | Media[] | Yes | A list of all search results from the given query. |
| total | number | Yes | Deprecated: Returns the count of results in the current response page. Use pagination info for navigation. |
| pagination | Pagination | Yes | Pagination information for cursor-based navigation. |
Media object
| Key | Type | Required | Description |
|---|---|---|---|
| mediaId | number | No | Opaque media database row ID for efficient follow-up media.meta and media.image requests. |
| system | System | Yes | System which the media has been indexed under. |
| name | string | Yes | A human-readable version of the result's filename without a file extension. |
| path | string | Yes | Canonical indexed media path. Use with system.id for media.meta and media.image. |
| relativePath | string | No | Launcher-relative convenience path, when it can be derived. Not a stable media identity. |
| hasCover | boolean | Yes | Whether media-level or title-level image properties are available. |
| coverColor | string | No | Average colour of the cover thumbnail as #rrggbb, for a placeholder while the image loads. Omitted until Core has built a thumbnail for the media through media.image with a maxSize. |
| zapScript | string | Yes | ZapScript command to launch this media item. Includes the disambiguating tags inline (e.g. @Arcade/X-Men Vs. Street Fighter (region:eu) (builddate:1996-10-04)) so the written command resolves back to this specific variant. |
| tags | TagInfo[] | Yes | Array of tags associated with this media item. |
| disambiguatingTags | TagInfo[] | No | Subset of tags whose values differ across same-named siblings of this title, plus any tag that makes the file a distinct game (a ROM hack, homebrew or public-domain work) even when it has no sibling, ordered by display importance. Omitted when there is nothing to disambiguate. Clients can render these to tell variants apart. |
System object
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | No | Internal system ID for this system. |
| name | string | No | Display name of the system. |
| category | string | No | Deprecated: use categories. Primary category of system (e.g., "Console", "Computer"), kept for clients that support one category. Always equal to the first entry of categories. |
| categories | string[] | No | Complete ordered category membership. The primary category is first, followed by configured additional memberships. |
| releaseDate | string | No | Release date of the system in ISO 8601 format (YYYY-MM-DD). |
| manufacturer | string | No | Manufacturer of the system (e.g., "Nintendo", "Sega"). |
| mediaCount | number | No | Populated only in systems responses; not included on System objects nested in media.search results. Exact non-missing indexed media-row count for this system, or exact matching count when systems.tags is set. Zero means the system is supported but empty. Omitted by older Core versions or when counts are unavailable. |
Clients should use categories and fall back to [category] only when connected to older Core versions that do not send it. Custom category strings are literal display values configured in Core TOML. Core matches configured references case-insensitively but returns canonical built-in spelling or custom declaration spelling. References to undeclared categories are ignored, and a virtual system whose primary category is undeclared reports Other. Clients display unknown custom values as received.
Available categories remain dynamic: derive them from categories on systems returned by the current request. Core does not return a separate fixed category catalog. Filters such as systems.tags may therefore change which categories are represented.
Pagination object
| Key | Type | Required | Description |
|---|---|---|---|
| nextCursor | string | No | Cursor for the next page of results. Omitted if no more pages available. |
| hasNextPage | boolean | Yes | Whether there are more results available after the current page. |
| pageSize | number | Yes | Number of results requested for this page (matches maxResults parameter). |
TagInfo object
| Key | Type | Required | Description |
|---|---|---|---|
| tag | string | Yes | The tag name. |
| type | string | Yes | The type/category of the tag (e.g., "genre", "year"). |
Example
Request
{
"jsonrpc": "2.0",
"id": "47f80537-7a5d-11ef-9c7b-020304050607",
"method": "media.search",
"params": {
"query": "240p"
}
}
Response
{
"jsonrpc": "2.0",
"id": "47f80537-7a5d-11ef-9c7b-020304050607",
"result": {
"results": [
{
"mediaId": 123,
"name": "240p Test Suite (PD) v0.03 tepples",
"path": "/media/fat/games/Gameboy/240p Test Suite (PD) v0.03 tepples.gb",
"relativePath": "Gameboy/240p Test Suite (PD) v0.03 tepples.gb",
"hasCover": false,
"zapScript": "@Gameboy/240p Test Suite (PD) v0.03 tepples",
"system": {
"category": "Handheld",
"categories": ["Handheld"],
"id": "Gameboy",
"name": "Gameboy"
},
"tags": [
{
"tag": "homebrew",
"type": "release"
}
]
}
],
"total": 1,
"pagination": {
"hasNextPage": false,
"pageSize": 100
}
}
}
Example with tag filtering
Request
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-7a5d-11ef-9c7b-020304050607",
"method": "media.search",
"params": {
"query": "mario",
"tags": ["genre:action:platformer", "publisher:nintendo"],
"maxResults": 10
}
}
Response
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-7a5d-11ef-9c7b-020304050607",
"result": {
"results": [
{
"mediaId": 456,
"name": "Super Mario Bros.",
"path": "/media/fat/games/NES/Super Mario Bros.nes",
"relativePath": "NES/Super Mario Bros.nes",
"hasCover": true,
"zapScript": "@NES/Super Mario Bros. (year:1985)",
"system": {
"category": "Console",
"categories": ["Console"],
"id": "NES",
"name": "Nintendo Entertainment System"
},
"tags": [
{
"tag": "action:platformer",
"type": "genre"
},
{
"tag": "nintendo",
"type": "publisher"
},
{
"tag": "1985",
"type": "year"
}
]
}
],
"total": 1,
"pagination": {
"hasNextPage": false,
"pageSize": 10
}
}
}
media.browse
Access: All clients.
Browse indexed media content by directory, similar to navigating a file manager. Supports filesystem paths, virtual URI schemes (e.g. mame-arcade://), and paginated results.
When called without a path parameter (or with an empty path), returns top-level root entries including filesystem roots and virtual scheme roots. When systems is provided without path, returns populated launcher routes for those systems only. Pass the same systems filter when browsing a returned route to keep shared paths scoped to the selected systems.
path also accepts the launcher-relative form reported as relativePath: a system ID, optionally followed by a path below that system's launcher folder (for example SNES or SNES/USA). Core browses the first matching folder, in platform root order, that holds indexed content, and the result's path is that folder's absolute path. A relative path that matches no indexed folder is an error. This lets a client keep a folder reference that survives the media moving to another root.
Set rootView to contents with exactly one system to replace its filesystem routes with a one-level view of their immediate contents. This is display-only: entries retain physical paths, and browsing a returned directory uses ordinary single-path behavior. Root priority follows platform order (first root wins); exact, case-sensitive filesystem basenames define collisions. Virtual URI routes remain separate.
A directory whose direct contents collapse to a single logical launch target is returned with that target's mediaId, display name, zapScript, tags, and hasCover, so a per-game disc folder appears as one launchable game. Display names leave out set markers such as (Disc 1); the disc number is reported in tags instead. A directory qualifies when it has no nested media and holds one media file, one .m3u plus its discs, one .cue plus its companion tracks, or at least two supported disc-image files that all share one positive title identity. Shared-title disc sets support .cue, .chd, .iso, .bin, .img, and .pbp; mixed title identities or other extensions remain ambiguous. The selected target is deterministic by path then media ID. Existing single-file, playlist, and cue precedence remains unchanged. Its type stays directory and it keeps its own path and fileCount, so clients can still navigate into it. Directories that hold nested media or an ambiguous file set stay plain directories.
A directory that collapsed because it holds several disc images of one game is marked multiDisc: true. Its launch fields point at the first disc and its disambiguatingTags omit the disc number, since the entry stands for the whole set. To offer the other discs, browse the entry's path.
Request a directory entry's image from media.image with its (system, path), not its mediaId. The path form returns the folder's own artwork first and falls back to the launch target's artwork; the mediaId form only ever returns the launch target's.
Plain directories may also have artwork imported by the media-folder scraper. This does not make them launchable or hide their children; it only sets hasCover and lets clients request the image with the directory's (system, path).
A directory holding media for more than one system also stays plain, because its fileCount is the sum across those systems and the rule is applied one system at a time. A page spanning several systems is resolved per system when systems names them; without a systems filter such a page is left unresolved, since browsing a media root lists one directory per installed system and resolving all of them is disproportionate to that page's cost.
Tags filter direct media files in the current path. Directories remain visible for navigation with tag-unfiltered fileCount values, while totalFiles, file pagination, and cursors reflect only matching files. Tagged directory entries remain plain directories rather than being promoted to logical single-game aliases.
Visibility is separate from ordinary tag filtering: hidden media is excluded from files, directory/root counts, and letter indexes before pagination. Hidden-only directories/routes disappear. Set includeHidden: true to show hidden entries with their user:hidden tag. Required user tag filters, such as user:favorite, user:liked or user:hidden, also include hidden entries. Changing visibility mode or editing media preferences invalidates existing browse cursors. A request with a cursor must restart without one when Core reports library visibility changed. A request with no cursor is retried once inside Core automatically and only returns an error if preferences change again during that retry.
Parameters
All parameters are optional. When called with no parameters, returns root entries.
| Key | Type | Required | Description |
|---|---|---|---|
| path | string | No | Directory path to browse. Omit or set empty to list root entries. Supports filesystem paths, launcher-relative paths (e.g. SNES/USA), and virtual URI schemes (e.g. mame-arcade://). |
| systems | string[] | No | Case-sensitive list of system IDs to restrict route discovery and browse results to. A missing key or empty list preserves unfiltered behavior. |
| fuzzySystem | boolean | No | Enable fuzzy matching for system IDs in the systems array (e.g., "snes" matches "SNES"). |
| includeHidden | boolean | No | Include hidden media and its contribution to directory/root counts. Defaults to false. Repeat with cursor requests. |
| rootView | string | No | Pathless system-root presentation: routes (default) returns separate populated routes; contents returns one-level immediate contents and requires exactly one system. Ignored when path is non-empty. Repeat with cursor requests. |
| maxResults | number | No | Maximum results per page. Default is 100, maximum is 1000. |
| cursor | string | No | Opaque pagination cursor from a previous response's nextCursor. Omit for first page. Cursors are valid only with the same path, systems, tags, letter, and sort parameters. |
| tags | string[] | No | Filter direct media files by tags. Syntax and AND/NOT/OR operators match media.search. Directories remain unfiltered. |
| letter | string | No | Filter results to entries starting with this letter. |
| sort | string | No | Sort order. One of: name-asc (default), name-desc, filename-asc, filename-desc. Name sorting is prefix-aware for detected ranked/date collection folders. The filename variants sort by full file path. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| path | string | Yes | The browsed directory path. Empty string when listing roots. |
| relativePath | string | No | Launcher-relative path of the browsed directory itself (for example SNES/USA). Present only when systems names exactly one system and the directory sits under that system's launcher folders. |
| entries | BrowseEntry[] | Yes | Array of entries in the current path. |
| totalFiles | number | Yes | Total count of media files in the current directory (respects tags and letter filters). |
| totalDirs | number | Yes | Total count of immediate child directories in the current directory. |
| pagination | Pagination | No | Pagination info. Omitted when there are no file results. |
Browse entry object
| Key | Type | Required | Description |
|---|---|---|---|
| mediaId | number | No | Opaque media database row ID. Present on media entries, and on directory entries whose direct contents collapse to one logical launch target, where it identifies that target. Use it for follow-up media.meta requests and for media.image on media entries; request a directory entry's image by (system, path) so folder artwork is used. |
| name | string | Yes | Display name of the entry. Set markers such as (Disc 1) are left out of media names and reported in tags. |
| path | string | Yes | Full path to the entry. |
| type | string | Yes | Entry type: root, directory, or media. |
| fileCount | number | No | Number of files in this directory. Present on root and directory entries, except a root entry whose exact count could not be computed in time (known non-empty, count omitted). |
| group | string | No | Launcher group name. Present on virtual scheme root entries. |
| systemId | string | No | System ID for the media or single-system filtered route (e.g. SNES). Present on media entries and filtered root entries when exactly one system applies. |
| systemIds | string[] | No | System IDs represented by a filtered root or directory entry. |
| zapScript | string | No | ZapScript command to launch this media. Present on media entries and logical single-game container directory entries. |
| relativePath | string | No | Launcher-relative convenience path (for example SNES/Game.sfc) when portable conversion succeeds. Present on media and logical single-game container entries, and on directory and root entries that belong to exactly one system, where it names the folder itself (SNES/USA, or SNES for the system's launcher folder) and can be sent back as path. Omitted for unmatched absolute paths, virtual URIs, and folders shared by several systems. Not a stable media identity. |
| tags | object[] | No | Tags attached to the media. Each object has tag (string) and type (string). Present on media entries and logical single-game container directory entries. |
| disambiguatingTags | object[] | No | Subset of tags whose values differ across same-named siblings of this title, plus any tag that makes the file a distinct game (a ROM hack, homebrew or public-domain work) even when it has no sibling, ordered by display importance. Same object shape as tags. Omitted when there is nothing to disambiguate. |
| multiDisc | boolean | No | true on a directory entry that stands for several disc images of one game. Its launch fields point at the first disc; browse its path to list the others. Omitted otherwise. |
| hasCover | boolean | Yes | Whether image properties are available. For directories this includes path-keyed folder artwork and, when collapsed, media/title artwork. Clients can skip image requests when false. |
| coverColor | string | No | Average colour of the cover thumbnail as #rrggbb, for a placeholder while the image loads. Present on media entries and logical single-game container directory entries once Core has built a thumbnail for the media through media.image with a maxSize. Omitted on a container directory entry that has folder artwork of its own, because the colour describes the launch target's cover. |
Browse pagination object
| Key | Type | Required | Description |
|---|---|---|---|
| hasNextPage | bool | Yes | Whether more results exist beyond the current page. |
| pageSize | number | Yes | The requested page size. |
| nextCursor | string | No | Opaque cursor for the next page. Absent on the last page. |
System route example
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "media.browse",
"params": {
"systems": ["SNES"]
}
}
Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"path": "",
"entries": [
{
"name": "SNES",
"path": "/roms/SNES",
"type": "root",
"fileCount": 150,
"hasCover": false,
"systemId": "SNES",
"systemIds": ["SNES"],
"relativePath": "SNES"
}
],
"totalFiles": 0
}
}
Root contents example
With SNES media under /configured/SNES and /roms/SNES, this view displays their immediate children together. If both roots contain the same basename, /configured/SNES wins because it appears first in platform root order.
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "media.browse",
"params": {
"systems": ["SNES"],
"rootView": "contents"
}
}
Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"path": "",
"entries": [
{
"name": "RPGs",
"path": "/configured/SNES/RPGs",
"type": "directory",
"fileCount": 42,
"systemIds": ["SNES"],
"hasCover": false
},
{
"mediaId": 42,
"name": "Super Mario World",
"path": "/roms/SNES/Super Mario World.sfc",
"type": "media",
"systemId": "SNES",
"zapScript": "@SNES/Super Mario World",
"relativePath": "SNES/Super Mario World.sfc",
"hasCover": true
}
],
"pagination": {
"hasNextPage": false,
"pageSize": 100
},
"totalDirs": 1,
"totalFiles": 1
}
}
Selecting RPGs browses only /configured/SNES/RPGs; rootView does not merge lower levels.
Browse path example
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "media.browse",
"params": {
"path": "/roms/SNES",
"maxResults": 3
}
}
Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"path": "/roms/SNES",
"entries": [
{
"name": "RPGs",
"path": "/roms/SNES/RPGs",
"type": "directory",
"fileCount": 42,
"hasCover": false
},
{
"mediaId": 42,
"name": "Super Mario World",
"path": "/roms/SNES/Super Mario World.sfc",
"type": "media",
"systemId": "SNES",
"hasCover": true,
"zapScript": "@SNES/Super Mario World",
"relativePath": "SNES/Super Mario World.sfc",
"tags": [
{"tag": "1990", "type": "year"},
{"tag": "2", "type": "players"}
]
},
{
"mediaId": 43,
"name": "The Legend of Zelda - A Link to the Past",
"path": "/roms/SNES/The Legend of Zelda - A Link to the Past.sfc",
"type": "media",
"systemId": "SNES",
"hasCover": false,
"zapScript": "@SNES/The Legend of Zelda - A Link to the Past",
"relativePath": "SNES/The Legend of Zelda - A Link to the Past.sfc",
"tags": [
{"tag": "1991", "type": "year"},
{"tag": "1", "type": "players"}
]
}
],
"totalFiles": 150,
"pagination": {
"hasNextPage": true,
"pageSize": 3,
"nextCursor": "eyJzb3J0VmFsdWUiOiJUaGUgTGVnZW5kIG9mIFplbGRhIC0gQSBMaW5rIHRvIHRoZSBQYXN0IiwibGFzdElkIjo0Mn0="
}
}
}
media.browse.index
Access: All clients.
Return the ordered first-character "jump to letter" buckets for a browse scope. Each bucket carries a count and a ready-to-use cursor that seeks media.browse to the start of that bucket, so a single round trip gives a client everything it needs to draw a section rail and jump into the full ordered list. This avoids paging from the top to reach a distant section, which matters on constrained clients (e.g. MiSTer).
The scope parameters mirror media.browse so the index describes the exact media-file list media.browse would return for the same scope. The per-bucket cursor is an ordinary browse cursor: pass it to media.browse with the same path/systems/tags/sort to get a normal page that begins at the bucket and continues into the next bucket as the user scrolls.
Parameters
All parameters are optional.
| Key | Type | Required | Description |
|---|---|---|---|
| path | string | No | Directory or virtual scheme to index, same as media.browse. Omit or set empty for a root listing (no rail applies). |
| systems | string[] | No | Case-sensitive system IDs to scope the index to, same as media.browse. |
| fuzzySystem | boolean | No | Enable fuzzy matching for system IDs in systems. |
| includeHidden | boolean | No | Match the visibility mode of media.browse. Defaults to false. |
| tags | string[] | No | Filter indexed media by tags, using the same syntax and operators as media.browse. |
| sort | string | No | Sort order, must match the media.browse sort the rail is for. One of name-asc (default), name-desc, filename-asc, filename-desc. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| scheme | string | Yes | Collation used to derive the buckets. latin for first-character bucketing; none when no rail applies (a root listing, or a directory whose effective sort is not alphabetical, e.g. a ranked/date-prefixed collection folder), in which case groups is empty. |
| totalFiles | number | Yes | Total media files matching the complete systems/path/tags scope. |
| groups | BrowseIndexGroup[] | Yes | Only non-empty buckets, ordered to match sort. |
Browse index group object
| Key | Type | Required | Description |
|---|---|---|---|
| key | string | Yes | Stable bucket identifier (A–Z, 0-9, #). Treat as opaque. |
| label | string | Yes | Display text for the bucket. Equal to key for the latin scheme. |
| count | number | Yes | Number of media files in the bucket. |
| cursor | string | Yes | Opaque media.browse cursor positioned just before the bucket's first row. Empty string for the bucket that begins the list (call media.browse with no cursor for the first page). |
| offset | number | Yes | 0-based position of the bucket's first item among the scope's media files, taken from its row number in the same ordered listing media.browse pages through (so it cannot drift from the browse order). Excludes any directory entries the listing shows before files; a client that jumps to a position in the full list adds its own leading-directory count. Use this to jump to the bucket's position rather than reloading from cursor. |
Clients should render groups exactly as received, in order, without assuming a particular alphabet: scheme and key are opaque so a future locale-aware scheme (e.g. pinyin/kana/hangul buckets) requires no client change.
Example
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "media.browse.index",
"params": {
"path": "/roms/SNES",
"sort": "name-asc"
}
}
Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"scheme": "latin",
"totalFiles": 150,
"groups": [
{ "key": "#", "label": "#", "count": 3, "cursor": "", "offset": 0 },
{ "key": "0-9", "label": "0-9", "count": 7, "cursor": "eyJzb3J0VmFsdWUiOiIjV29sZiIsImxhc3RJZCI6MTAyfQ==", "offset": 3 },
{ "key": "A", "label": "A", "count": 12, "cursor": "eyJzb3J0VmFsdWUiOiI5IExpdmVzIiwibGFzdElkIjoxMTV9", "offset": 10 }
]
}
}
To jump to "A", the client calls media.browse with that group's cursor and the same path/sort; the returned page begins at the first "A" title and continues into "B" as the user keeps scrolling.
media.tags
Access: All clients.
Query the media database and return available tags for filtering.
This method returns all available tags (with their types) for the specified systems. Use this to build dynamic filter UIs showing available tag options.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| systems | string[] | No | Case-sensitive list of system IDs to restrict tags to. A missing key or empty list will get all systems. |
| fuzzySystem | boolean | No | Enable fuzzy matching for system IDs in the systems array (e.g., "snes" matches "SNES"). |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| tags | TagInfo[] | Yes | Array of available tags. |
Tag Capping: To prevent large responses, long-tail tag types are capped at 100 entries
per type. Tags within each type are sorted by usage count (most popular first), then
alphabetically. Every tag type is either a closed list of canonical values, a strict format,
or one of the three free-text company types (developer, publisher, credit). Closed
types such as genre, region, lang and arcadeboard have finite vocabularies and are
returned in full, as are year and rating. The free-text types, the other format types
(for example extension, track, mameparent, builddate) and search, whose franchise
and feature values run long, are capped.
TagInfo object
| Key | Type | Required | Description |
|---|---|---|---|
| tag | string | Yes | The tag value. |
| type | string | Yes | The tag type (e.g., "genre", "year"). |
Example
Request
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5d-11ef-9c7b-020304050607",
"method": "media.tags",
"params": {
"systems": ["NES", "SNES"]
}
}
Response
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5d-11ef-9c7b-020304050607",
"result": {
"tags": [
{
"type": "genre",
"tag": "action"
},
{
"type": "genre",
"tag": "action:platformer"
},
{
"type": "search",
"tag": "franchise:castlevania"
}
]
}
}
media.tags.update
Access: All clients.
Add or remove user tags for an indexed media item.
Mutable tags are user:favorite, user:hidden, user:liked, user:disliked and user:playlater. All are installation-wide preferences available to all clients, not security restrictions. Add user:hidden to hide an entry; remove it to unhide. When the same tag appears in both lists, addition wins. Editing one flag preserves the others, except for the pairs the model forbids: adding user:disliked clears user:liked and user:favorite, and adding user:liked or user:favorite clears user:disliked. A request that adds both sides of such a pair at once is rejected. Deck membership tags (user:deck:<id>) are read-only here and managed through the decks methods.
Hidden entries disappear from normal discovery and random selection, but remain launchable through direct NFC, ZapScript, playlists, and explicit API launches. Favorites, the other user tag lists and history retain hidden entries and include the user:hidden tag when their current media tags are available. includeHidden: true on browse/search enables recovery.
All flags persist in UserDB and are restored to MediaDB on reindex/rebuild by canonical system/path, like existing favorites. Moving a file does not transfer any preference; the old path's preferences remain stored. Automatic reassociation is deferred. Successful hide/unhide emits media.visibility, prompting connected clients to refresh their lists and discard old cursors.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| mediaId | number | No | Media DBID to update. Cannot be mixed with system/path. |
| system | string | No | System ID for path-based lookup. Required when using path. |
| path | string | No | Media path for path-based lookup. Required with system. |
| add | string[] | No | Tags to add, from the mutable user tags above. |
| remove | string[] | No | Tags to remove, from the mutable user tags above. |
Either mediaId or system plus path is required. At least one of add or remove is required. Search operators (+, -, ~) are not valid in mutation requests.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| tags | TagInfo[] | Yes | Effective tags for the media item. |
Example
Request
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5d-11ef-9c7b-020304050607",
"method": "media.tags.update",
"params": {
"mediaId": 42,
"add": ["user:favorite"]
}
}
Response
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5d-11ef-9c7b-020304050607",
"result": {
"tags": [
{
"type": "user",
"tag": "favorite"
}
]
}
}
media.generate
Access: All clients.
Create a new media database index.
During indexing, the server will emit media.indexing notifications showing progress of the index.
Parameters
Optionally, an object:
| Key | Type | Required | Description |
|---|---|---|---|
| systems | string[] | No | List of system IDs to restrict indexing to. Other system indexes will remain as is. |
| fuzzySystem | boolean | No | Enable fuzzy matching for system IDs in the systems array (e.g., "snes" matches "SNES"). |
| rebuild | boolean | No | Discard the media database entirely and index from scratch ("fresh start"). Scraped metadata is lost and must be re-scraped; favourites and launcher overrides are preserved (they live in the user database and are re-applied after indexing). Cannot be combined with systems. |
An omitted or null value parameters key is also valid and will index every system.
Selective Indexing Behavior:
- When
systemsis provided with specific system IDs, only those systems will be reindexed - The server will validate all provided system IDs and return an error if any are invalid
- If all systems are specified (equivalent to no restriction), a full database rebuild will be performed for optimal performance
- Selective indexing cannot be performed while database optimization is running
- Resume functionality will validate that the system configuration hasn't changed between indexing sessions
Result
Returns null on success. Indexing runs in the background after the response is sent. Track progress using media.indexing notifications.
Examples
Full index request
{
"jsonrpc": "2.0",
"id": "6f20e07c-7a5e-11ef-84bb-020304050607",
"method": "media.generate"
}
Response
{
"jsonrpc": "2.0",
"id": "6f20e07c-7a5e-11ef-84bb-020304050607",
"result": null
}
Selective index request
{
"jsonrpc": "2.0",
"id": "7f30e17d-7a5e-11ef-85cc-020304050607",
"method": "media.generate",
"params": {
"systems": ["NES", "SNES", "Genesis"]
}
}
Response
{
"jsonrpc": "2.0",
"id": "7f30e17d-7a5e-11ef-85cc-020304050607",
"result": null
}
media.generate.cancel
Access: All clients.
Cancel any currently running media database indexing operation.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| message | string | Yes | Status message about the cancellation. |
Example
Request
{
"jsonrpc": "2.0",
"id": "8f40e28e-7a5e-11ef-86dd-020304050607",
"method": "media.generate.cancel"
}
Response (indexing was running)
{
"jsonrpc": "2.0",
"id": "8f40e28e-7a5e-11ef-86dd-020304050607",
"result": {
"message": "Media indexing cancelled successfully"
}
}
Response (no indexing running)
{
"jsonrpc": "2.0",
"id": "8f40e28e-7a5e-11ef-86dd-020304050607",
"result": {
"message": "No media indexing operation is currently running"
}
}
media.generate.resume
Access: All clients.
Resume media database indexing paused by Core while media is active.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| message | string | Yes | Media indexing resumed when a paused index resumes, or Media indexing is not paused when there is nothing to resume. |
Example
Request
{
"jsonrpc": "2.0",
"id": "9a51f39f-7a5e-11ef-87ee-020304050607",
"method": "media.generate.resume"
}
Response
{
"jsonrpc": "2.0",
"id": "9a51f39f-7a5e-11ef-87ee-020304050607",
"result": {
"message": "Media indexing resumed"
}
}
media.active
Access: All clients.
Returns the currently active media.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| slot | string | No | Media slot to query. Use primary or background. Defaults to primary. |
Result
Returns an ActiveMedia object if media is currently active, or null if no media is active.
Example
Request
{
"jsonrpc": "2.0",
"id": "47f80537-7a5d-11ef-9c7b-020304050607",
"method": "media.active"
}
Response (no active media)
{
"jsonrpc": "2.0",
"id": "47f80537-7a5d-11ef-9c7b-020304050607",
"result": null
}
Response (media active)
{
"jsonrpc": "2.0",
"id": "47f80537-7a5d-11ef-9c7b-020304050607",
"result": {
"mediaId": 42,
"started": "2024-09-24T17:49:42.938167429+08:00",
"launcherId": "SNES",
"systemId": "SNES",
"systemName": "Super Nintendo Entertainment System",
"mediaPath": "/roms/snes/Super Mario World (USA).sfc",
"relativePath": "snes/Super Mario World (USA).sfc",
"mediaName": "Super Mario World",
"zapScript": "@SNES/Super Mario World",
"launcherControls": ["load_state", "save_state", "toggle_menu"]
}
}
media.active.update
Access: All clients.
Update the currently active media information.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| systemId | string | Yes | ID of the system. |
| mediaPath | string | Yes | Path to the media file. |
| mediaName | string | Yes | Display name of the media. |
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "47f80537-7a5d-11ef-9c7b-020304050607",
"method": "media.active.update",
"params": {
"systemId": "SNES",
"mediaPath": "/roms/snes/game.sfc",
"mediaName": "Game"
}
}
Response
{
"jsonrpc": "2.0",
"id": "47f80537-7a5d-11ef-9c7b-020304050607",
"result": null
}
media.history.latest
Access: All clients.
Return the most recent played media entry from the user database only. This is intended for startup paths that need the last played game as quickly as possible, without media database enrichment.
This method does not return tags, metadata, media IDs, ZapScript, pagination, end time, or play time.
Parameters
None. Empty params may be omitted or sent as {}.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| entry | MediaHistoryLatestEntry | Yes | Most recent media play history entry, or null when none exists. |
Media history latest entry object
| Key | Type | Required | Description |
|---|---|---|---|
| systemId | string | Yes | ID of the system. |
| systemName | string | Yes | Display name of the system from the history row. |
| mediaName | string | Yes | Display name of the media from the history row. |
| mediaPath | string | Yes | Path to the media file from the history row. |
| relativePath | string | No | Launcher-relative convenience path, when it can be derived. Not a stable media identity. |
| launcherId | string | Yes | ID of the launcher used. |
| startedAt | string | Yes | Timestamp when media started in RFC3339 format. |
Example
Request
{
"jsonrpc": "2.0",
"id": "9f2c6a52-7a5d-11ef-9c7b-020304050607",
"method": "media.history.latest"
}
Response
{
"jsonrpc": "2.0",
"id": "9f2c6a52-7a5d-11ef-9c7b-020304050607",
"result": {
"entry": {
"systemId": "SNES",
"systemName": "Super Nintendo Entertainment System",
"mediaName": "Super Mario World",
"mediaPath": "/roms/snes/Super Mario World (USA).sfc",
"relativePath": "SNES/Super Mario World (USA).sfc",
"launcherId": "SNES",
"startedAt": "2025-01-22T14:30:00Z"
}
}
}
media.history
Access: All clients.
Return paginated media play history. Set distinctMedia to return only the newest session for each (systemId, mediaPath) identity, which is useful for recents grids.
Parameters
Optionally, an object:
| Key | Type | Required | Description |
|---|---|---|---|
| limit | number | No | Maximum number of entries to return. Default is 25, maximum is 100. |
| cursor | string | No | Cursor for pagination. Omit for first page, use nextCursor from previous response for subsequent pages with the same filters and distinctMedia value. |
| systems | string[] | No | Filter to one or more system IDs (e.g., ["SNES", "NES"]). |
| fuzzySystem | boolean | No | Enable fuzzy matching for system IDs. |
| distinctMedia | boolean | No | Return the newest session for each unique (systemId, mediaPath) pair. Each page contains up to limit unique media entries. Default is false. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| entries | MediaHistoryEntry[] | Yes | A list of media play history entries. |
| pagination | Pagination | No | Pagination information for cursor-based navigation. Only present when entries are returned. |
Media history entry object
| Key | Type | Required | Description |
|---|---|---|---|
| mediaId | number | No | Opaque media database row ID for efficient follow-up media.meta and media.image requests. Omitted when the history path cannot be resolved in the current media database. |
| systemId | string | Yes | ID of the system. |
| systemName | string | Yes | Display name of the system. |
| mediaName | string | Yes | Display name of the media. |
| mediaPath | string | Yes | Path to the media file. |
| relativePath | string | No | Launcher-relative convenience path, when it can be derived. Not a stable media identity. |
| zapScript | string | No | ZapScript command to launch this media item, in the same form media.search reports. Omitted when the history path cannot be resolved in the current media database. |
| hasCover | boolean | Yes | Whether media-level or title-level image properties are available. |
| coverColor | string | No | Average colour of the cover thumbnail as #rrggbb, for a placeholder while the image loads. Omitted until Core has built a thumbnail for the media through media.image with a maxSize. |
| launcherId | string | Yes | ID of the launcher used. |
| startedAt | string | Yes | Timestamp when media started in RFC3339 format. |
| endedAt | string | No | Timestamp when media stopped in RFC3339 format. Omitted if media is still active. |
| playTime | number | Yes | Duration of the play session in seconds. |
| sessionSource | string | Yes | How the session was timed: active_media (Core's own launch lifecycle), foreground_events (host foreground evidence, with user-granted permission) or host_return (until the launcher came back, without that permission). |
| sessionConfidence | string | Yes | unspecified for a row predating this distinction, exact for measured foreground time, approximate for a host_return estimate that never proves the game was played, or provisional for a foreground_events launch not yet confirmed (no endedAt, playTime 0; it becomes exact or is withdrawn). |
| tags | TagInfo[] | No | Tags for the resolved media, merged from file-level and title-level tags exactly as media.search returns them. An empty array means the media is indexed but has no tags. Omitted when mediaId is omitted or when media database enrichment fails or times out. |
Example
Request
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5d-11ef-9c7b-020304050607",
"method": "media.history",
"params": {
"limit": 10,
"distinctMedia": true
}
}
Response
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5d-11ef-9c7b-020304050607",
"result": {
"entries": [
{
"mediaId": 42,
"systemId": "SNES",
"systemName": "Super Nintendo Entertainment System",
"mediaName": "Super Mario World",
"mediaPath": "/roms/snes/Super Mario World (USA).sfc",
"relativePath": "snes/Super Mario World (USA).sfc",
"zapScript": "@SNES/Super Mario World",
"hasCover": true,
"launcherId": "SNES",
"startedAt": "2025-01-22T14:30:00Z",
"endedAt": "2025-01-22T15:15:30Z",
"playTime": 2730,
"sessionSource": "foreground_events",
"sessionConfidence": "exact",
"tags": [
{ "tag": "favorite", "type": "user" },
{ "tag": "action:platformer", "type": "genre" }
]
}
],
"pagination": {
"hasNextPage": false,
"pageSize": 10
}
}
}
media.history.top
Access: All clients.
Return aggregated media play history grouped by game, sorted by total play time descending. Useful for "most played" displays.
Parameters
Optionally, an object:
| Key | Type | Required | Description |
|---|---|---|---|
| limit | number | No | Maximum number of entries to return. Default is 25, maximum is 100. |
| systems | string[] | No | Filter to one or more system IDs (e.g., ["SNES", "NES"]). |
| fuzzySystem | boolean | No | Enable fuzzy matching for system IDs. |
| since | string | No | Only count sessions starting after this RFC3339 timestamp. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| entries | MediaHistoryTopEntry[] | Yes | A ranked list of games by total play time. |
Media history top entry object
| Key | Type | Required | Description |
|---|---|---|---|
| mediaId | number | No | Opaque media database row ID for efficient follow-up media.meta and media.image requests. Omitted when the history path cannot be resolved in the current media database. |
| systemId | string | Yes | ID of the system. |
| systemName | string | Yes | Display name of the system. |
| mediaName | string | Yes | Display name of the media. |
| mediaPath | string | Yes | Path to the media file (from most recent session). |
| relativePath | string | No | Launcher-relative convenience path, when it can be derived. Not a stable media identity. |
| zapScript | string | No | ZapScript command to launch this media item, in the same form media.search reports. Omitted when the history path cannot be resolved in the current media database. |
| totalPlayTime | number | Yes | Total play time across all sessions in seconds. |
| sessionCount | number | Yes | Number of play sessions. |
| lastPlayedAt | string | Yes | Timestamp of the most recent session in RFC3339 format. |
| tags | TagInfo[] | No | Tags for the resolved media, merged from file-level and title-level tags exactly as media.search returns them. An empty array means the media is indexed but has no tags. Omitted when mediaId is omitted or when media database enrichment fails or times out. |
Example
Request
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-8b6e-12f0-ad8c-030405060708",
"method": "media.history.top",
"params": {
"limit": 5,
"systems": ["SNES"]
}
}
Response
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-8b6e-12f0-ad8c-030405060708",
"result": {
"entries": [
{
"mediaId": 42,
"systemId": "SNES",
"systemName": "Super Nintendo Entertainment System",
"mediaName": "Super Mario World",
"mediaPath": "/roms/snes/Super Mario World (USA).sfc",
"relativePath": "snes/Super Mario World (USA).sfc",
"zapScript": "@SNES/Super Mario World",
"totalPlayTime": 7200,
"sessionCount": 12,
"lastPlayedAt": "2026-02-14T20:30:00Z",
"tags": [
{ "tag": "favorite", "type": "user" },
{ "tag": "action:platformer", "type": "genre" }
]
}
]
}
}
media.lookup.candidates
Returns up to five ranked canonical title candidates for an approximate name in one system. This is title discovery, not media selection: pass a selected candidate's systemId and name to media.lookup for file selection and enrichment.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| system | string | Yes | One canonical system ID; never a list or an all-systems search. |
| name | string | Yes | Approximate title, 1–256 Unicode characters. Whitespace-only names and names that normalize to an empty slug are rejected. |
| fuzzySystem | boolean | No | Resolve system names/aliases instead of requiring a canonical ID. Default false, matching media.lookup. |
| maxResults | integer | No | Maximum number of candidates, 1–5. Default 5; out-of-range values are rejected. |
Result
{"candidates": [...]}; an empty array means no eligible title met the match threshold. Database failures and canceled requests remain errors, not empty results.
Each candidate contains only:
| Key | Type | Description |
|---|---|---|
| systemId | string | Canonical system ID. |
| name | string | Canonical indexed title, deduplicated within the system. |
| rank | integer | One-based rank; results are ordered by rank. |
| matchType | string | Stable coarse enum: exact, secondary, or fuzzy. |
| confidence | number | Advisory ranking evidence, not a probability or a stable client threshold contract. |
Exact normalized primary-title evidence ranks before exact secondary-title evidence. If any eligible primary or secondary exact matches exist, only those matches are returned: results are never padded with fuzzy matches to reach maxResults. Fuzzy matching runs only when neither exact class produces an eligible result, and reuses title normalization and the existing length/word-count prefilter, token-order matching, and typo threshold. Within an evidence class, ranking uses confidence, edit-distance tie-breaking for fuzzy matches, then canonical name and an internal identity tie-breaker. Internal algorithm names are not API enums. These candidates need not reproduce the launch resolver's prefix, progressive-trimming, tag preference, or media-selection fallbacks.
A title is eligible only if at least one indexed media entry is present and not user-hidden. Hidden/missing variants do not suppress another eligible variant. Hidden state is always excluded; there is no includeHidden override. Authorization matches ordinary media discovery; this method grants no launch or profile-management authority. Results never contain paths, media IDs, tags, artwork, or ZapScript, and requests neither launch nor update history or lookup/resolution caches.
Results describe the current MediaDB incarnation, not a durable catalog snapshot. A request pins one database connection and fails with a retryable error if a fresh-start rebuild replaces that database before the final generation check. Cached IDs from a discarded database are not used against its replacement. Ordinary indexing and hide/unhide changes may become visible between read statements; exact matches are read directly from indexed SQL without waiting for a shared slug cache refresh, while fuzzy discovery can lag behind ordinary indexing until that refresh. The later media.lookup resolves against its own then-current state, so candidates do not reserve a file or guarantee later availability.
{
"jsonrpc": "2.0",
"id": 1,
"method": "media.lookup.candidates",
"params": {"system": "NES", "name": "Metriod"}
}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"candidates": [
{"systemId": "NES", "name": "Metroid", "rank": 1, "matchType": "fuzzy", "confidence": 0.96}
]
}
}
The example confidence is illustrative; clients should present ordered candidates, not hard-code score cutoffs.
media.lookup
Access: All clients.
Resolve a game name and system to a media database match.
Given a system ID and game name, searches the media database for the best matching title. Uses fuzzy matching to handle minor differences in naming. Returns null for the match when no title is found or confidence is too low.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| system | string | Yes | System ID to search within (e.g., "SNES", "Genesis"). |
| name | string | Yes | Game name to look up. |
| fuzzySystem | boolean | No | Enable fuzzy matching for the system ID (e.g., "snes" matches "SNES"). |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| match | MediaLookupMatch | No | The best matching media entry, or null if no match found. |
Media lookup match object
| Key | Type | Required | Description |
|---|---|---|---|
| mediaId | number | No | Opaque media database row ID for efficient follow-up media.meta and media.image requests. |
| system | System | Yes | System the media was found in. |
| name | string | Yes | Display name of the matched media. |
| path | string | Yes | Path to the media file. |
| relativePath | string | No | Launcher-relative convenience path, when it can be derived. Not a stable media identity. |
| zapScript | string | Yes | ZapScript command to launch this media item. |
| tags | TagInfo[] | Yes | Array of tags associated with this media item. |
| confidence | number | Yes | Match confidence score from 0.0 to 1.0. |
Example
Request
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-7a5d-11ef-9c7b-020304050607",
"method": "media.lookup",
"params": {
"system": "SNES",
"name": "Super Mario World"
}
}
Response (match found)
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-7a5d-11ef-9c7b-020304050607",
"result": {
"match": {
"mediaId": 42,
"system": {
"id": "SNES",
"name": "Super Nintendo Entertainment System",
"category": "Console",
"categories": ["Console"],
"releaseDate": "1990-11-21",
"manufacturer": "Nintendo"
},
"name": "Super Mario World",
"path": "/roms/snes/Super Mario World (USA).sfc",
"relativePath": "SNES/Super Mario World (USA).sfc",
"zapScript": "@SNES/Super Mario World",
"tags": [
{
"tag": "action:platformer",
"type": "genre"
},
{
"tag": "1990",
"type": "year"
}
],
"confidence": 0.95
}
}
}
Response (no match)
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-7a5d-11ef-9c7b-020304050607",
"result": {
"match": null
}
}
media.meta
Access: All clients.
Return the full metadata graph for one indexed media row, including its title, system, tags, and scraped properties.
Use this when a client has a search, browse, or lookup result and needs all metadata attached to that row. Identify media by the result's mediaId when available, or by system.id and canonical path. Launcher-relative paths in the system/path shape are accepted as a compatibility fallback when they resolve to exactly one indexed media row. Properties are separated by scope: media.properties applies to the specific ROM/file row, and media.title.properties applies to the shared title.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| mediaId | number | No | Opaque media database row ID from search, browse, or lookup. Cannot be mixed with system/path. |
| system | string | No | System ID for the media row. Required when mediaId is omitted. |
| path | string | No | Canonical indexed media path. Required when mediaId is omitted. |
| items | object[] | No | Batch request items. Each item uses either mediaId or system/path. Maximum 100 items. Cannot be mixed with top-level media ref fields. |
Single requests return the existing single media response shape. Batch requests return { "items": [...] } in input order. Each batch item contains either media or error, so one missing media row does not fail the whole batch.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| media | MediaMeta | Yes | Metadata for the media row. |
Media meta object
| Key | Type | Required | Description |
|---|---|---|---|
| path | string | Yes | Media file path. |
| relativePath | string | No | Launcher-relative convenience path, when it can be derived. Not a stable media identity. |
| zapScript | string | No | ZapScript command to launch this media item, in the same form media.search reports. Omitted for a missing media row. |
| parentDir | string | Yes | Parent directory stored for the media row. |
| isMissing | boolean | Yes | Whether the indexed file is currently missing. |
| tags | TagInfo[] | Yes | ROM-level tags for this media row. |
| properties | object | Yes | ROM-level properties keyed by canonical type tag. |
| launcherOverride | string | No | Launcher ID stored for this media row, mirrored from property:launcher-override in properties. When present, Core uses it for title, search, path, random, and history launches unless ZapScript includes an explicit launcher argument. |
| title | MediaMetaTitle | Yes | Shared title metadata for this media row. |
Media meta title object
| Key | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Primary normalized title slug. |
| secondarySlug | string | No | Secondary title slug, when available. |
| name | string | Yes | Display title. |
| slugLength | number | Yes | Character length of the primary slug. |
| slugWordCount | number | Yes | Word count of the primary slug. |
| system | object | Yes | Stored system object with id and name. |
| tags | TagInfo[] | Yes | Title-level tags shared by matching media rows. |
| properties | object | Yes | Title-level properties keyed by canonical type tag. |
Media meta property object
| Key | Type | Required | Description |
|---|---|---|---|
| text | string | Yes | Text value or source path for the property. |
| contentType | string | Yes | MIME type for binary-backed properties, empty for text-only values. |
| extension | string | No | File extension without a dot, derived from MIME type or source path. |
| blobSize | number | No | Size in bytes for binary-backed properties. |
Binary property data is not returned by media.meta. Use media.image to fetch image bytes and media.asset to fetch supported file-backed assets.
Property keys are canonical type tags such as property:description, property:image-image, or property:manual.
Example
Request
{
"jsonrpc": "2.0",
"id": "d4e5f6a7-7a5d-11ef-9c7b-020304050607",
"method": "media.meta",
"params": {
"system": "SNES",
"path": "/roms/snes/Super Mario World.sfc"
}
}
Response
{
"jsonrpc": "2.0",
"id": "d4e5f6a7-7a5d-11ef-9c7b-020304050607",
"result": {
"media": {
"path": "/roms/snes/Super Mario World.sfc",
"relativePath": "SNES/Super Mario World.sfc",
"zapScript": "@SNES/Super Mario World",
"parentDir": "/roms/snes",
"isMissing": false,
"tags": [
{"type": "region", "tag": "us"}
],
"properties": {
"property:launcher-override": {
"text": "RetroArch",
"contentType": ""
}
},
"launcherOverride": "RetroArch",
"title": {
"slug": "super mario world",
"name": "Super Mario World",
"slugLength": 17,
"slugWordCount": 3,
"system": {
"id": "SNES",
"name": "Super Nintendo Entertainment System"
},
"tags": [
{"type": "developer", "tag": "nintendo"},
{"type": "genre", "tag": "action:platformer"}
],
"properties": {
"property:description": {
"text": "Mario's dinosaur friend Yoshi makes his debut.",
"contentType": ""
}
}
}
}
}
}
Batch Request
{
"jsonrpc": "2.0",
"id": "d4e5f6a7-7a5d-11ef-9c7b-020304050608",
"method": "media.meta",
"params": {
"items": [
{"mediaId": 42},
{"system": "SNES", "path": "/roms/snes/Super Metroid.sfc"}
]
}
}
media.meta.update
Access: All clients.
Update writable metadata fields for one indexed media row, then return the same response shape as media.meta.
Use this to store a per-media launcher override. Core validates the launcher exists and supports the media row's system before saving it. Set launcherOverride to null to clear the override.
Launcher selection order is:
- Explicit
launcheradvanced argument in ZapScript. - Per-media
launcherOverridestored withmedia.meta.update. - System default launcher from configuration.
- Normal launcher matching.
Parameters
An object identifying the media row by mediaId or by system and canonical path.
| Key | Type | Required | Description |
|---|---|---|---|
| mediaId | number | No | Opaque media database row ID from search, browse, or lookup. Cannot be mixed with system/path. |
| system | string | No | System ID for the media row. Required when mediaId is omitted. |
| path | string | No | Canonical indexed media path. Required when mediaId is omitted. |
| media | object | Yes | Patch object. Currently supports only launcherOverride. |
Media patch object
| Key | Type | Required | Description |
|---|---|---|---|
| launcherOverride | string|null | Yes | Launcher ID to use for this media row, matched case-insensitively and stored with canonical casing. Use null to clear it. Empty strings are rejected. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| media | MediaMeta | Yes | Updated metadata for row. |
Example
Set override
{
"jsonrpc": "2.0",
"id": 1,
"method": "media.meta.update",
"params": {
"mediaId": 42,
"media": {
"launcherOverride": "RetroArch"
}
}
}
Clear override
{
"jsonrpc": "2.0",
"id": 2,
"method": "media.meta.update",
"params": {
"system": "SNES",
"path": "/roms/snes/Super Mario World.sfc",
"media": {
"launcherOverride": null
}
}
}
media.asset
Access: All clients.
Return one bounded base64 chunk from an allowlisted file-backed property associated with indexed media. Initial support is assetType: "manual", which resolves property:manual and accepts only regular PDF files within trusted media or scraper roots. video and other assets are not currently supported. Source paths and basenames are never returned.
Each request is independent. Start with offset: 0, then request nextOffset with the exact etag from the preceding response until complete is true. Assets no larger than one requested chunk return with complete: true in first response; clients must stop when complete is true. This pull model keeps memory and WebSocket messages bounded and gives clients natural backpressure. If an asset changes between chunks, Core rejects the continuation; restart from offset zero.
Asset resolution follows media metadata precedence: primary media property, equivalent media aliases, then title property.
Parameters
An object identifying a media row by mediaId or by (system, path).
| Key | Type | Required | Description |
|---|---|---|---|
| mediaId | number | No | Opaque media database row ID from search, browse, or lookup. Cannot be mixed with system/path. |
| system | string | No | System ID. Required when mediaId is omitted. |
| path | string | No | Canonical indexed media path. Required when mediaId is omitted. |
| assetType | string | Yes | Allowlisted asset type. Currently only manual. Raw property tags are not accepted. |
| offset | number | No | Zero-based raw byte offset. Defaults to 0. Nonzero offsets require etag. An offset equal to size returns an empty complete chunk; larger offsets are rejected. |
| length | number | No | Requested raw bytes. Defaults to and cannot exceed 131072 (128 KiB). |
| etag | string | No | Opaque asset version from a prior response. Required when offset is nonzero. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| assetType | string | Yes | Resolved allowlisted asset type. |
| typeTag | string | Yes | Canonical matched property tag; property:manual for manuals. |
| contentType | string | Yes | Validated MIME type; application/pdf for manuals. |
| extension | string | Yes | Validated extension without a dot; pdf for manuals. |
| size | number | Yes | Total raw asset size in bytes. |
| etag | string | Yes | Opaque asset version. Send unchanged on continuation requests. |
| offset | number | Yes | Raw byte offset returned by this response. |
| length | number | Yes | Actual raw bytes in data. May be smaller than requested at EOF. |
| data | string | Yes | Base64-encoded chunk bytes. Empty at exact EOF. |
| nextOffset | number | No | Offset for next request. Omitted when complete is true. |
| complete | boolean | Yes | Whether this response reaches asset EOF. |
Example
Initial request
{
"jsonrpc": "2.0",
"id": 1,
"method": "media.asset",
"params": {
"mediaId": 123,
"assetType": "manual",
"length": 131072
}
}
Partial response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"assetType": "manual",
"typeTag": "property:manual",
"contentType": "application/pdf",
"extension": "pdf",
"size": 7340032,
"etag": "43e47c399d35d8be...",
"offset": 0,
"length": 131072,
"data": "JVBERi0xLjcK...",
"nextOffset": 131072,
"complete": false
}
}
Continuation request
{
"jsonrpc": "2.0",
"id": 2,
"method": "media.asset",
"params": {
"mediaId": 123,
"assetType": "manual",
"offset": 131072,
"length": 131072,
"etag": "43e47c399d35d8be..."
}
}
media.image
Access: All clients.
Return the best matching image for one indexed media row or indexed directory. Inline base64 delivery remains default. Clients can explicitly request a transient path to a Core-owned cached thumbnail.
For path requests, media.image preserves exact-media behavior first, then checks path-keyed directory properties, then tries existing launcher-relative and singleton media fallbacks. Within media results it checks each requested image type against media-level properties before title-level properties. Directory properties follow the same requested type order. If a stored media file path no longer exists, the stale property is removed and lookup continues; stale directory paths remain until the next completed media-folder snapshot.
Parameters
An object identifying a media row by mediaId or identifying media/directory content by (system, path). Canonical indexed paths are preferred. Directory artwork requires the directory's exact indexed path. Launcher-relative paths in the system/path shape are accepted as a compatibility fallback when they resolve to exactly one indexed media row.
| Key | Type | Required | Description |
|---|---|---|---|
| mediaId | number | No | Opaque media database row ID from search, browse, or lookup. Cannot be mixed with system/path. |
| system | string | No | System ID. Required when mediaId is omitted. |
| path | string | No | Canonical indexed media or directory path. Required when mediaId is omitted. |
| imageTypes | string[] | No | Image type preference order. Defaults to image, thumbnail, boxart, boxart3d, screenshot, wheel, titleshot, map, marquee, fanart. |
| maxSize | number | No | Longest-edge size hint in pixels. When set, the server resizes the image to fit a maxSize×maxSize box and caches the result; omit it for the full-size image. Required for localPath delivery. |
| delivery | string | No | inline (default) or localPath. localPath requires a positive maxSize and returns a path on the Core host. |
Supported image type values are image, thumbnail, boxart, boxart3d, screenshot, wheel, titleshot, map, marquee, and fanart. They resolve to canonical property tags such as property:image-image and property:image-boxart.
Resizing is intended for grid and preview views where transferring and holding full-size art is expensive. maxSize is snapped up to the nearest of a small set of standard tiers (32, 64, 128, 256, 512, 768) server-side. The returned image is never larger than the snapped tier and never larger than the source — when the source already fits the tier it is returned at its native dimensions, so the result may still be larger than the exact maxSize you asked for. Request your true display size (logical size × pixel ratio) and downscale to the final size on the client. The snapped tiers bound how many resized variants are cached per image. Output is re-encoded as WebP (lossy, alpha preserved) regardless of source format — including when the source already fits the box, so even a near-native request still gets the smaller WebP — and cached on disk so repeat requests are cheap. The original bytes are kept only when WebP would not shrink them (already-compact sources), when maxSize is omitted/non-positive (full size), or when the source cannot be decoded.
When a resized thumbnail is built for a request whose image type preference list has more than one entry, Core records the image type it resolved to and the thumbnail's average colour. Later requests, including after a restart, are then served from the thumbnail cache without reading the original artwork, and list results (media.browse, media.search, media.history) report the colour as coverColor. A request for a single image type does not change the recorded cover. The records are cleared together with the thumbnail cache after indexing or scraping changes artwork.
localPath never returns an original scraper or media path. Core resolves image semantics, materializes its own bounded thumbnail cache artifact, and returns that path. Path delivery is available to any client that explicitly requests it, regardless of peer locality or Core platform; remote callers are responsible for having an appropriate shared-filesystem view of the Core host path. Treat the path as opaque, transient, and nonportable: read it immediately, never persist it or derive neighboring paths, and retry once with delivery: "inline" if the file is inaccessible or disappears before it is opened. If cache materialization fails, Core can safely return delivery: "inline" in the same response.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| delivery | string | Yes | Actual delivery used: inline or localPath. Clients must inspect this field because a requested local path can fall back inline. |
| contentType | string | Yes | MIME type of the returned image data. |
| extension | string | No | File extension without a dot, derived from MIME type or source path. |
| data | string | No | Base64-encoded image bytes. Present for inline delivery. |
| localPath | string | No | Absolute, opaque Core-host path to a cached thumbnail. Present for localPath delivery. |
| typeTag | string | Yes | Canonical property tag that matched. |
Example
Request
{
"jsonrpc": "2.0",
"id": "e5f6a7b8-7a5d-11ef-9c7b-020304050607",
"method": "media.image",
"params": {
"system": "SNES",
"path": "/roms/snes/Super Mario World.sfc",
"imageTypes": ["boxart", "image"],
"maxSize": 512
}
}
Response
{
"jsonrpc": "2.0",
"id": "e5f6a7b8-7a5d-11ef-9c7b-020304050607",
"result": {
"delivery": "inline",
"contentType": "image/webp",
"extension": "webp",
"data": "UklGRiQAAABXRUJQVlA4...",
"typeTag": "property:image-boxart"
}
}
Local-path request
{
"jsonrpc": "2.0",
"id": "e5f6a7b8-7a5d-11ef-9c7b-020304050607",
"method": "media.image",
"params": {
"mediaId": 123,
"imageTypes": ["boxart"],
"maxSize": 256,
"delivery": "localPath"
}
}
Local-path response
{
"jsonrpc": "2.0",
"id": "e5f6a7b8-7a5d-11ef-9c7b-020304050607",
"result": {
"delivery": "localPath",
"contentType": "image/webp",
"extension": "webp",
"localPath": "/media/fat/zaparoo/cache/thumbs/v2/U05FUw/example.webp",
"typeTag": "property:image-boxart"
}
}
scrapers
Access: All clients.
List all registered metadata scrapers.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| scrapers | ScraperInfo[] | Yes | Registered scraper implementations. |
Scraper info object
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Stable scraper ID used by media.scrape. |
| name | string | Yes | Human-readable scraper name. |
| supportedSystems | string[] | Yes | Supported system IDs. Empty means the scraper can run against all systems. |
Example
Request
{
"jsonrpc": "2.0",
"id": "f6a7b8c9-7a5d-11ef-9c7b-020304050607",
"method": "scrapers"
}
Response
{
"jsonrpc": "2.0",
"id": "f6a7b8c9-7a5d-11ef-9c7b-020304050607",
"result": {
"scrapers": [
{
"id": "gamelist.xml",
"name": "ES gamelist.xml",
"supportedSystems": []
},
{
"id": "media-folder",
"name": "ES media folders",
"supportedSystems": []
},
{
"id": "mister-docs",
"name": "MiSTer docs databases",
"supportedSystems": []
}
]
}
}
media.scrape
Access: All clients.
Start a metadata scraper run in the background.
Scraping enriches existing MediaDB records only. It does not create media rows; run media.generate first so the filesystem scanner has indexed the library. Scraping and media indexing are mutually exclusive, and only one scraper can run at a time.
Progress is reported with media.scraping notifications and can be queried with media.scrape.status. Scraping pauses while media is running and resumes automatically when playback stops.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| scraperId | string | Yes | Scraper ID from the scrapers method, for example gamelist.xml. |
| systems | string[] | No | System IDs to scrape. Omit or pass an empty array to scrape all eligible systems. Cannot be combined with scope. |
| scope | object | No | Select one indexed media item or one directory subtree; see below. |
| force | boolean | No | Re-scrape records that already have this scraper's sentinel tag, within the selected scope. Default is false. |
Scope
Without scope (or with scope: null), existing systems behavior is unchanged. Otherwise, supply exactly one of these forms:
{"scope": {"mediaId": 42}}
{"scope": {"file": {"system": "SNES", "path": "/games/SNES/Game.sfc"}}}
{"scope": {"subtree": {"system": "SNES", "path": "/games/SNES/RPG"}}}
These are parameter fragments; scraperId is still required.
mediaIdmust be a positive indexed media ID, available frommedia.searchormedia.browsebefore scraping. Use it when available; clients that only have a file path can usefileinstead.filematches one indexed file or virtual URI exactly. It does not resolve a directory to its launch target.subtreeselects indexed files below a directory recursively, within the specified system./games/foodoes not select/games/foobar. A filesystem or volume root is valid.systemaccepts canonical IDs and existing system aliases, case-insensitively, and must identify an indexed system. Path spelling is case-sensitive on all platforms, including Windows; use the indexed spelling. Windows native separators and forward slashes are accepted. On Unix, backslashes are literal filename characters.- Filesystem paths must be absolute. Relative paths,
..components, control characters, invalid UTF-8, and paths longer than 4096 bytes are rejected. Repeated native separators,.components, and trailing separators are normalized. Paths are matched lexically against the index; symlinks are not resolved and directories are not scanned. - Virtual URIs such as
steam://123are opaque, exact identities supported bymediaIdorfile; they are not valid subtree paths. Whether metadata exists depends on the selected scraper's sources. - Missing or index-marked-missing IDs/files are client errors. A valid subtree with no present indexed matches succeeds with zero work, even if that directory no longer exists. Unindexed media must be indexed first.
- Empty scope objects, unknown scope fields, multiple scope forms, and combining
scopewithsystems(including[]) are client errors. Selector arrays are not supported. Duplicate source matches do not scrape a selected media row more than once per run. - Scoped progress counts selected media rows:
totalis selection size; unmatched, already-scraped, and already-completed-on-resume rows count as skipped.totalScrapedretains its existing scraper/library-wide meaning. Source files may still need parsing even when only one media row is selected. - Metadata and cleanup writes stay within selected media and their shared titles. Title-level metadata is shared with other ROMs of that title, so those ROMs may display updated title metadata too.
- Interrupted operations persist normalized scope and force-run markers. Restart recovery restores that scope, never a whole-system fallback. Single-item scopes also pin system and path alongside the ID; an identity that disappeared or changed fails recovery. Subtrees are re-queried within the same stored boundary, not snapshotted as an ID list.
- Status, cancellation, and playback pause/resume remain operation-wide. Cancellation discards resumable operation state;
media.scrape.resumeresumes a playback-paused run, not a cancelled run. IDs are local to the current media database and are not portable across rebuilds.
Result
Returns null on success. The scraper continues after the response is sent.
Example
Request
{
"jsonrpc": "2.0",
"id": "a7b8c9d0-7a5d-11ef-9c7b-020304050607",
"method": "media.scrape",
"params": {
"scraperId": "gamelist.xml",
"systems": ["SNES", "NES"],
"force": false
}
}
Response
{
"jsonrpc": "2.0",
"id": "a7b8c9d0-7a5d-11ef-9c7b-020304050607",
"result": null
}
media.scrape.status
Access: All clients.
Return the latest known metadata scraper status.
This method behaves like media does for indexing status: clients can query the current scrape snapshot after opening a UI, then continue listening for media.scraping notifications. If no scrape has run since startup, the result is idle with scraping: false, done: false, and state: "idle". Existing flat counter fields remain for compatibility; new UIs should prefer currentSystem for per-system progress and totalSteps/currentStep/currentStepDisplay for whole-run progress.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| scraperId | string | No | Scraper ID for the latest or active run. |
| systemId | string | No | System currently being processed, when known. |
| processed | integer | Yes | Number of records processed. |
| total | integer | Yes | Total records expected for the current scrape, when known. |
| matched | integer | Yes | Number of records matched and enriched. |
| skipped | integer | Yes | Number of records skipped. |
| totalScraped | integer | Yes | Number of media records already marked scraped. |
| scraping | boolean | Yes | Whether a scrape is currently running. |
| done | boolean | Yes | Whether the latest scrape reached a terminal state. |
| paused | boolean | Yes | Whether the active scrape is paused because media is running or until resumed. |
| state | string | No | Explicit lifecycle state: idle, running, paused, completed, cancelled, or failed. |
| error | string | No | Fatal scrape error on failed terminal updates. |
| totalSteps | integer | No | Total systems in the scrape run, when known. |
| currentStep | integer | No | 1-based current system step, when known. |
| currentStepDisplay | string | No | Display name for the current system step, falling back to system ID. |
| currentSystem | object | No | Per-system progress object with systemId, systemName, processed, total, matched, and skipped. |
Example
Request
{
"jsonrpc": "2.0",
"id": "b8c9d0e1-7a5d-11ef-9c7b-020304050607",
"method": "media.scrape.status"
}
Response
{
"jsonrpc": "2.0",
"id": "b8c9d0e1-7a5d-11ef-9c7b-020304050607",
"result": {
"scraperId": "gamelist.xml",
"systemId": "snes",
"processed": 42,
"total": 100,
"matched": 38,
"skipped": 4,
"totalScraped": 1200,
"scraping": true,
"done": false,
"paused": false,
"state": "running",
"totalSteps": 2,
"currentStep": 1,
"currentStepDisplay": "Super Nintendo Entertainment System",
"currentSystem": {
"systemId": "snes",
"systemName": "Super Nintendo Entertainment System",
"processed": 42,
"total": 100,
"matched": 38,
"skipped": 4
}
}
}
media.scrape.cancel
Access: All clients.
Cancel the currently running metadata scraper operation.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| message | string | Yes | Status message about the cancellation. |
Example
Request
{
"jsonrpc": "2.0",
"id": "b8c9d0e1-7a5d-11ef-9c7b-020304050607",
"method": "media.scrape.cancel"
}
Response
{
"jsonrpc": "2.0",
"id": "b8c9d0e1-7a5d-11ef-9c7b-020304050607",
"result": {
"message": "scraping cancelled"
}
}
media.scrape.resume
Access: All clients.
Resume a paused metadata scraper operation.
Scraping normally resumes automatically when playback stops. This method mirrors media.generate.resume and lets a local client force the active scrape to continue while the pauser is currently paused.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| message | string | Yes | Status message about resuming. |
Example
Request
{
"jsonrpc": "2.0",
"id": "c9d0e1f2-7a5d-11ef-9c7b-020304050607",
"method": "media.scrape.resume"
}
Response
{
"jsonrpc": "2.0",
"id": "c9d0e1f2-7a5d-11ef-9c7b-020304050607",
"result": {
"message": "Media scraping resumed"
}
}
media.clean.orphans
Access: All clients.
Delete media rows marked missing and remove orphaned related data.
This is intended for cleanup after files have been removed from disk and the media database has been refreshed. It removes missing Media rows, their tags and properties, and any titles that no longer have media rows. It does not run VACUUM; SQLite will reuse freed pages.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| deleted | number | Yes | Number of missing media rows removed. |
Example
Request
{
"jsonrpc": "2.0",
"id": "c9d0e1f2-7a5d-11ef-9c7b-020304050607",
"method": "media.clean.orphans"
}
Response
{
"jsonrpc": "2.0",
"id": "c9d0e1f2-7a5d-11ef-9c7b-020304050607",
"result": {
"deleted": 12
}
}
media.control
Access: All clients.
Send a control action to the active media's launcher.
Requires active media with a launcher that supports control capabilities. The available control actions depend on the launcher. Use the launcherControls field from media.active or media to discover supported actions.
Control actions run in a restricted runtime that blocks media-launching and playlist commands. Utility commands like input.keyboard, execute, delay and echo are allowed. The execute command bypasses the allow_execute allowlist for control scripts defined in launcher configuration.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| action | string | Yes | The control action to execute (e.g., "save_state", "toggle_pause"). |
| slot | string | No | Target media slot. Omit for primary media; use "background" to control background audio. |
| args | object | No | Optional key-value arguments for the control action. Values are strings. |
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "c3d4e5f6-7a5d-11ef-9c7b-020304050607",
"method": "media.control",
"params": {
"action": "save_state"
}
}
Response
{
"jsonrpc": "2.0",
"id": "c3d4e5f6-7a5d-11ef-9c7b-020304050607",
"result": null
}
Slot and arguments example
This request targets the background slot and supplies an action argument. Discover supported actions through launcherControls.
{
"jsonrpc": "2.0",
"id": "d4e5f6a7-7a5d-11ef-9c7b-020304050607",
"method": "media.control",
"params": {
"action": "fast_forward",
"slot": "background",
"args": {
"seconds": "30"
}
}
}
media.title.parse
Access: All clients.
Preview title and slug generation for a media path without reading the filesystem or media database. This uses the same path parsing rules as media indexing.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| systemId | string | Yes | System ID used to select game or media title-parsing rules. |
| path | string | Yes | Media path to parse. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Parsed display title. |
| slug | string | Yes | Primary normalized title slug. |
| secondarySlug | string | No | Secondary slug generated for a subtitle when present. |
| slugLength | number | Yes | Primary slug length in Unicode characters. |
| slugWordCount | number | Yes | Number of words represented by primary slug. |
Example
Request
{
"jsonrpc": "2.0",
"id": "c4e5f607-7a5d-11ef-9c7b-020304050607",
"method": "media.title.parse",
"params": {
"systemId": "NES",
"path": "roms/nes/Tetris.nes"
}
}
Response
{
"jsonrpc": "2.0",
"id": "c4e5f607-7a5d-11ef-9c7b-020304050607",
"result": {
"name": "Tetris",
"slug": "tetris",
"slugLength": 6,
"slugWordCount": 1
}
}
systems
Access: All clients.
List systems currently indexed or supported by an available launcher on the running platform. Virtual systems are also included.
Set all to include every system represented by the running platform's launcher definitions, even when its runtime dependency is currently unavailable. This is useful when selecting a specific system for its first media index. On MiSTer, a launcher whose FPGA core isn't installed on the SD card counts as unavailable, so without all the system list reflects only systems you can currently launch. See launchers to check which core a launcher needs.
Responses include each system's complete categories membership, primary category first, and the deprecated category field. Additional custom memberships come from manual Core TOML configuration; this API does not provide category mutation or UI behavior. Clients derive available categories from systems in the response.
Responses include an exact non-missing mediaCount for each system when the media database count query succeeds. Supported systems with no indexed media have mediaCount: 0. The field is omitted if counts are unavailable, preserving compatibility with older clients and database-error fallback behavior.
Set tags to return only systems containing matching non-missing media. Tagged responses use mediaCount for the exact matching count and omit zero-match systems. Tag syntax and AND/NOT/OR operators match media.search. Tags remain the final filter when combined with all, so launcher-only systems with no matching media are omitted.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| includeHidden | boolean | No | Include hidden media in system counts. Defaults to false; required user tag filters also include hidden entries. |
| all | boolean | No | Include systems with unavailable launchers. Defaults to false. Indexed systems remain listed. |
| tags | string[] | No | Return systems with matching media. Uses the same tag syntax and operators as media.search. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| systems | System[] | Yes | Indexed, available, and optionally unavailable platform systems. Tagged requests include only positive-count systems. |
See System object.
Example
Request
{
"jsonrpc": "2.0",
"id": "dbd312f3-7a5f-11ef-8f29-020304050607",
"method": "systems",
"params": {
"all": true
}
}
Response
{
"jsonrpc": "2.0",
"id": "dbd312f3-7a5f-11ef-8f29-020304050607",
"result": {
"systems": [
{
"id": "GameboyColor",
"name": "Gameboy Color",
"category": "Handheld",
"categories": ["Handheld"],
"releaseDate": "1998-10-21",
"manufacturer": "Nintendo",
"mediaCount": 842
},
{
"id": "EDSAC",
"name": "EDSAC",
"category": "Computer",
"categories": ["Computer", "Favorite Systems"],
"releaseDate": "1949-05-06",
"manufacturer": "University of Cambridge",
"mediaCount": 0
}
]
}
}
Settings
settings
Access: All accepted clients. Online backup, play-history sync, and remote-control fields are returned only to localhost or authenticated admin.
List currently set configuration settings.
This method will list values set in the Config File. Some config file options may be omitted which are not appropriate to be read or written remotely.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| runZapScript | boolean | Yes | The user's ZapScript setting. Holds do not change it. |
| debugLogging | boolean | Yes | Whether debug logging is enabled. |
| audioScanFeedback | boolean | Yes | Whether audio feedback on scan is enabled. |
| readersAutoDetect | boolean | Yes | Whether automatic reader detection is enabled. |
| readersScanMode | string | Yes | Current scan mode setting. |
| readersScanExitDelay | number | Yes | Delay before exiting scan mode in seconds. |
| readersScanIgnoreSystems | string[] | Yes | List of system IDs to ignore during scanning. |
| errorReporting | boolean | Yes | Whether error reporting is enabled. |
| encryption | boolean | Yes | Whether paired encryption is required for remote WebSocket connections. Localhost remains exempt. |
| readersConnect | ReaderConnection[] | Yes | List of manually configured reader connections. |
| systemDefaults | SystemDefault[] | Yes | Per-system overrides for default launcher and exit ZapScript. |
| profilesRequireForLaunch | boolean | Yes | Whether media launches are blocked while no personal profile is active. |
| profilesSwapData | boolean | Yes | Whether profile switches also swap profile-scoped data (saves, save states) on supported platforms. Defaults to true. |
| updateChannel | string | Yes | Release channel used for update checks: stable or beta. Defaults to stable. |
| updateCheck | boolean | Yes | Whether the service looks for new releases on its own. Defaults to true on every platform, including installs a package manager owns. |
| updateInstall | boolean | Yes | Whether the device downloads and installs updates on its own, rather than only telling the user one exists. Defaults to false, and is always false while updateCheck is off. |
| backupRemoteEnabled | boolean | No | Whether automatic remote backup scheduling is enabled. Only returned to localhost and authenticated admin clients. |
| playtimeSyncEnabled | boolean | No | Whether the user explicitly enabled play history sync. Defaults to false. Only returned to localhost and authenticated admin clients. |
| librarySyncEnabled | boolean | No | Whether the user explicitly enabled Library sync. Defaults to false, and is reset to false whenever the account is linked or unlinked. Only returned to localhost and authenticated admin clients. |
| backupRemoteSchedule | string | No | Remote backup schedule: daily, weekly, or manual. Only returned to localhost and authenticated admin clients. |
| remoteControlEnabled | boolean | No | Whether the device owner explicitly allowed a linked Zaparoo Online account to send remote commands to this device. Defaults to false, and is reset to false whenever the account is linked or unlinked. Only returned to localhost and authenticated admin clients. |
| onlineBaseUrl | string | No | Base URL of the Zaparoo Online service every online feature uses: account linking, cloud backup, play history sync, Library sync and remote control (read-only, set as online_base_url under [service] in the config file). Only returned to localhost and authenticated admin clients. |
Reader connection object
| Key | Type | Required | Description |
|---|---|---|---|
| driver | string | Yes | Reader driver type (e.g., "pn532uart", "acr122pcsc"). |
| path | string | Yes | Path or address for the reader connection. |
| idSource | string | No | Source for the reader ID. |
| enabled | bool | No | Whether the connection is enabled. Defaults to true if omitted. |
| scanMode | string | No | Scan mode for this reader ("tap" or "hold"), overriding the driver's and the global readers.scan.mode. Empty means inherit. Case and surrounding space are ignored and the canonical spelling is stored; any other value is rejected. |
System default object
| Key | Type | Required | Description |
|---|---|---|---|
| system | string | Yes | System ID this default applies to. Accepts canonical IDs and aliases. |
| launcher | string | No | Launcher ID or group name to use for this system. Empty means no override. |
| beforeExit | string | No | ZapScript to run just before media for this system stops or is replaced: tapping another card, **stop, **playlist.stop, **mister.mgl, the stop method, a playtime limit, or a hold-mode card removal. Does not run when media exits on its own, and applies to primary media only. Failures are logged and never block the exit, the script is bounded to 30 seconds, and only one runs at a time. A before_exit can also be written per launcher or launcher group as [[launchers.default]] before_exit, or globally as [launchers] before_exit; neither is exposed through this API, and this per-system script beats a group entry and the global but loses to an entry naming the launcher that started the media. |
Example
Request
{
"jsonrpc": "2.0",
"id": "f208d996-7ae6-11ef-960e-020304050607",
"method": "settings"
}
Response
{
"jsonrpc": "2.0",
"id": "f208d996-7ae6-11ef-960e-020304050607",
"result": {
"runZapScript": true,
"debugLogging": false,
"audioScanFeedback": true,
"readersAutoDetect": true,
"readersScanMode": "tap",
"readersScanExitDelay": 0.0,
"readersScanIgnoreSystems": ["DOS"],
"errorReporting": true,
"encryption": false,
"readersConnect": [],
"systemDefaults": [
{
"system": "Genesis",
"launcher": "retroarch"
}
]
}
}
settings.update
Access: Requires settings.write. Changing encryption is localhost only. Online backup, play-history sync, and remote-control settings require localhost or authenticated admin, so legacy clients cannot change them.
Update one or more settings in-memory and save changes to disk.
This method will only write values which are supplied. Existing values will not be modified.
Parameters
An object containing any of the following optional keys:
| Key | Type | Required | Description |
|---|---|---|---|
| runZapScript | boolean | No | Whether ZapScript execution is enabled. |
| debugLogging | boolean | No | Whether debug logging is enabled. |
| audioScanFeedback | boolean | No | Whether audio feedback on scan is enabled. |
| readersAutoDetect | boolean | No | Whether automatic reader detection is enabled. |
| readersScanMode | string | No | Current scan mode setting. |
| readersScanExitDelay | number | No | Delay before exiting scan mode in seconds. |
| readersScanIgnoreSystems | string[] | No | List of system IDs to ignore during scanning. |
| errorReporting | boolean | No | Whether error reporting is enabled. |
| encryption | boolean | No | Require paired encryption for remote WebSocket connections. This setting can only be changed from localhost. |
| readersConnect | ReaderConnection[] | No | List of manually configured reader connections. |
| systemDefaults | SystemDefault[] | No | Replace the full list of per-system launcher/exit-script overrides. Each launcher value, if non-empty, must match a known launcher ID or group (case-insensitive). |
| profilesRequireForLaunch | boolean | No | Whether media launches are blocked while no personal profile is active. |
| profilesSwapData | boolean | No | Whether profile switches also swap profile-scoped data. Turning it off converges data back to the shared state immediately. |
| updateChannel | string | No | Release channel used for update checks: stable or beta. |
| updateCheck | boolean | No | Whether the service looks for new releases on its own. |
| updateInstall | boolean | No | Whether the device installs updates on its own. Setting it to true while update checking is off is refused; send updateCheck: true in the same call to turn both on. |
| backupRemoteEnabled | boolean | No | Enable automatic remote backup scheduling. Requires localhost or an authenticated admin client. |
| playtimeSyncEnabled | boolean | No | Explicitly enable or disable play history sync. The first enabled sync uploads retained local history. Disabling stops future uploads. Requires localhost or an authenticated admin client. |
| librarySyncEnabled | boolean | No | Explicitly enable or disable Library sync, which uploads the list of games this device holds and keeps favorites, play-later, likes, dislikes and decks in step with the linked Zaparoo Online account. Disabling removes this device's game list from the account; local favorites and decks are kept. The linked account is told within a minute. Requires localhost or an authenticated admin client. |
| backupRemoteSchedule | string | No | Remote backup schedule: daily, weekly, or manual. Requires localhost or an authenticated admin client. |
| remoteControlEnabled | boolean | No | Allow or stop allowing the linked Zaparoo Online account to send remote commands to this device. Takes effect within a few seconds; the device advertises or withdraws the capability itself. Requires localhost or an authenticated admin client. |
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"method": "settings.update",
"params": {
"debugLogging": false
}
}
Response
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"result": null
}
settings.zapscript.hold
Access: Localhost or settings.write. WebSocket only.
Disable ZapScript execution for as long as this WebSocket connection stays open. The hold is released when the connection closes for any reason, including a client that is killed, so it cannot leave ZapScript disabled the way settings.update with runZapScript: false can. Holds do not change the runZapScript setting. A connection holds at most once; repeat calls succeed without stacking.
Parameters
None.
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"method": "settings.zapscript.hold"
}
Response
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"result": null
}
settings.reload
Access: All clients.
Reload settings and mappings from disk.
Parameters
None.
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"method": "settings.reload"
}
Response
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"result": null
}
settings.auth.claim
Access: Unauthenticated bootstrap.
Redeem a claim token against a remote auth server and store the resulting credentials in auth.toml.
This method performs trust discovery using the .well-known/zaparoo protocol. It first verifies that the claim URL's root domain supports auth (auth: 1 in the well-known response), then redeems the claim token to obtain a bearer credential. If the root domain's well-known response includes a trusted list, each related domain is checked for bidirectional trust confirmation before extending the credential. Production claim URLs must use HTTPS. Plain HTTP is accepted only for loopback, private-network, or link-local development endpoints; public HTTP endpoints are rejected.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| claimUrl | string | Yes | HTTPS claim URL. HTTP is allowed only for loopback, private, or link-local development endpoints. |
| token | string | Yes | The one-time claim token to redeem. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| domains | string[] | Yes | List of domains the credential was stored for (root + any trusted). |
Example
Request
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-auth-claim-example",
"method": "settings.auth.claim",
"params": {
"claimUrl": "https://api.example.com/auth/claim",
"token": "claim-token-abc123"
}
}
Response
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-auth-claim-example",
"result": {
"domains": [
"https://api.example.com",
"https://cdn.example.com"
]
}
}
settings.auth.status
Access: Unauthenticated bootstrap.
Report whether Core holds a stored bearer credential for an auth server URL. The check is local only: the token is never validated against the server and no token material is returned.
Status probes are only answered for official Zaparoo API hosts over HTTPS and for the configured online base URL. Any other URL returns linked: false without revealing whether a credential exists.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | Auth server URL to check link state for. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| linked | boolean | Yes | Whether a stored bearer credential exists for the URL. |
Example
Request
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-auth-status-example",
"method": "settings.auth.status",
"params": {
"url": "https://api.zaparoo.com"
}
}
Response
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-auth-status-example",
"result": {
"linked": true
}
}
settings.auth.unlink
Access: Localhost or admin.
Remove the device's online account credentials — the inverse of settings.auth.link. The claim/link flow tags every credential it stores with the root domain that created it (linked_via in auth.toml), so unlink removes the configured remote backup server's entry plus every entry tagged with it, whatever domains the server's trusted list contained at link time. Credentials for other domains, hand-written basic-auth entries, and API keys are untouched. Remote backup state is marked unlinked so the status UI prompts a re-link and the scheduler stops attempting remote backups.
Requires a localhost client or an authenticated admin client, including a valid static API-key admin.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| domains | string[] | Yes | Domains whose stored credentials were removed. |
Example
Request
{
"jsonrpc": "2.0",
"id": "c3d4e5f6-auth-unlink-example",
"method": "settings.auth.unlink"
}
Response
{
"jsonrpc": "2.0",
"id": "c3d4e5f6-auth-unlink-example",
"result": {
"domains": ["https://api.zaparoo.com", "https://zpr.au"]
}
}
settings.auth.link
Access: Unauthenticated bootstrap.
Start a reverse device link flow (device-authorization style): Core requests a link from the auth server, returns a user code and verification URLs to display, then polls in the background until the user approves the link in their account. On approval the resulting claim token is redeemed through the same pipeline as settings.auth.claim and the credential is stored in auth.toml.
This method is deliberately available before client authentication. Remote HTTP POST still requires an address allowed by allowed_ips. Only one link flow can be pending at a time; starting another while one is pending returns an error. Progress is pushed via the auth.link.status notification (with user code and verification URLs omitted) and can be polled with settings.auth.link.status.
Parameters
An object (optional):
| Key | Type | Required | Description |
|---|---|---|---|
| url | string | No | Auth server base URL. Defaults to the configured online base URL, which is the official Zaparoo API unless changed. HTTP is allowed only for loopback, private, or link-local development endpoints. |
Result
A link status object:
| Key | Type | Required | Description |
|---|---|---|---|
| status | string | Yes | One of none, pending, approved, failed, or cancelled. |
| userCode | string | No | Short code the user enters at the verification URL. |
| verificationUrl | string | No | URL where the user approves the link. |
| verificationUrlComplete | string | No | Verification URL with the user code included, for QR display. |
| expiresAt | string | No | RFC 3339 time when the link request expires. |
| error | string | No | Human-readable reason when status is failed. |
Example
Request
{
"jsonrpc": "2.0",
"id": "c3d4e5f6-auth-link-example",
"method": "settings.auth.link"
}
Response
{
"jsonrpc": "2.0",
"id": "c3d4e5f6-auth-link-example",
"result": {
"status": "pending",
"userCode": "ABCD-1234",
"verificationUrl": "https://online.zaparoo.com/link",
"verificationUrlComplete": "https://online.zaparoo.com/link?code=ABCD1234",
"expiresAt": "2026-06-24T15:14:05Z"
}
}
settings.auth.link.status
Access: Unauthenticated bootstrap with redacted output; localhost and admin receive full output. Members are rejected.
Return the state of the active link flow as a link status object (see settings.auth.link). When no flow has been started, status is none.
Access is tiered: localhost, paired admin, and API-key admin receive the full object including userCode and verification URLs. Unauthenticated callers receive only flow state; userCode, verificationUrl, and verificationUrlComplete are omitted. Paired member clients are forbidden.
Parameters
None.
Result
A link status object (see settings.auth.link).
Example
Request
{
"jsonrpc": "2.0",
"id": "d4e5f6a7-auth-link-status-example",
"method": "settings.auth.link.status"
}
Response
{
"jsonrpc": "2.0",
"id": "d4e5f6a7-auth-link-status-example",
"result": {
"status": "approved"
}
}
settings.auth.link.cancel
Access: Localhost or admin.
Cancel the pending link flow. Requires a localhost client or an authenticated admin client, including a valid static API-key admin. Returns the terminal cancelled status object with user code and verification URLs omitted. When no flow is pending, returns an error (no active link request).
Parameters
None.
Result
A link status object (see settings.auth.link) with status set to cancelled.
Example
Request
{
"jsonrpc": "2.0",
"id": "e5f6a7b8-auth-link-cancel-example",
"method": "settings.auth.link.cancel"
}
Response
{
"jsonrpc": "2.0",
"id": "e5f6a7b8-auth-link-cancel-example",
"result": {
"status": "cancelled"
}
}
settings.logs.download
Access: All clients.
Download the current log file as base64-encoded content.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| filename | string | Yes | Name of the log file. |
| size | number | Yes | Size of the log file in bytes. |
| content | string | Yes | Base64-encoded content of the log file. |
Example
Request
{
"jsonrpc": "2.0",
"id": "9f50e39f-7a5e-11ef-87ee-020304050607",
"method": "settings.logs.download"
}
Response
{
"jsonrpc": "2.0",
"id": "9f50e39f-7a5e-11ef-87ee-020304050607",
"result": {
"filename": "zaparoo.log",
"size": 1024,
"content": "MjAyNC0wOS0yNFQxNzowMDowMC4wMDBaIElORk8gU3RhcnRpbmcgWmFwYXJvby4uLg=="
}
}
Device backup response objects
Backup methods use the following shared response objects. Authentication credentials are excluded from every backup. Restoring preserves the destination device's identity, encryption setting, paired clients, and stored credentials.
Local backup object
| Key | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Backup ZIP filename. |
| path | string | No | Backup ZIP path on the device. |
| createdAt | string | Yes | Creation time in RFC 3339 format. |
| size | number | Yes | ZIP size in bytes. |
| status | string | Yes | success or partial. partial means one or more files were skipped. |
| integrity | string | Yes | valid after creation or restore validation; unchecked after metadata-only inspection. |
| categories | object | No | Map from category name to backup category status. |
| warnings | BackupWarning[] | No | Files omitted from backup. |
| error | string | No | Safe error summary when available. |
Backup category status object
| Key | Type | Required | Description |
|---|---|---|---|
| files | number | Yes | Number of included files. |
| bytes | number | Yes | Total uncompressed bytes. |
| enabled | boolean | Yes | Whether category is enabled in backup scope. |
Backup warning object
| Key | Type | Required | Description |
|---|---|---|---|
| category | string | Yes | Backup category. |
| path | string | Yes | Affected source path. |
| reason | string | Yes | Why file was skipped. |
Remote backup object
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Opaque remote backup ID. |
| backupType | string | Yes | Backup type, such as manual or scheduled. |
| schemaVersion | number | Yes | Remote backup schema version. |
| createdAt | string | Yes | Creation time in RFC 3339 format. |
| sizeBytes | number | Yes | Stored backup size in bytes. |
| manifestHash | string | Yes | Hash identifying snapshot contents. |
| categories | object | Yes | Map from category name to { "files": number, "bytes": number }. |
| coreVersion | string | No | Core version that created backup. |
| platform | string | No | Source platform ID. |
| verifiedAt | string | No | Latest verification time in RFC 3339 format. |
| restoredAt | string | No | Latest restore time in RFC 3339 format. |
| sourceDevice | object | No | Source device: id, name, linked, current, and optional platform. IDs are opaque. |
| incompatible | boolean | No | Whether backup uses a newer schema and cannot be restored by this Core version. |
| manifest | object | No | Remote manifest when endpoint includes it. |
Local backups are ZIP archives using known categories (zaparoo, settings, inputs, saves, and savestates), exact platform matching, SHA-256 payload verification, and fixed entry, path, and manifest limits. Restores stage and verify every payload before device mutation. Remote files larger than a 64 MiB transfer pack are skipped and reported, and server quota is checked before upload.
settings.backup
Access: Localhost or admin.
Create a local full-device ZIP backup.
Parameters
None.
Result
A local backup object. Newly created backups report integrity: "valid".
Example
{
"jsonrpc": "2.0",
"id": "backup-create-1",
"method": "settings.backup"
}
{
"jsonrpc": "2.0",
"id": "backup-create-1",
"result": {
"name": "backup-20260710-120000-manual.zip",
"path": "/data/backups/backup-20260710-120000-manual.zip",
"createdAt": "2026-07-10T12:00:00Z",
"size": 1048576,
"status": "success",
"integrity": "valid",
"categories": {
"settings": {"files": 4, "bytes": 8192, "enabled": true}
}
}
}
settings.backup.list
Access: Localhost or admin.
List local backup ZIP metadata without reading archive manifests.
Parameters
None.
Result
An array of objects with name, createdAt, size, and optional device-local path.
Example
{
"jsonrpc": "2.0",
"id": "backup-list-1",
"method": "settings.backup.list"
}
{
"jsonrpc": "2.0",
"id": "backup-list-1",
"result": [
{
"name": "backup-20260710-120000-manual.zip",
"path": "/data/backups/backup-20260710-120000-manual.zip",
"createdAt": "2026-07-10T12:00:00Z",
"size": 1048576
}
]
}
settings.backup.inspect
Access: Localhost or admin.
Read and validate local backup manifest metadata without hashing every payload.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Local backup filename from settings.backup.list. |
Result
A local backup object. Successful inspection reports integrity: "unchecked"; restore performs full payload verification before mutation.
Example
{
"jsonrpc": "2.0",
"id": "backup-inspect-1",
"method": "settings.backup.inspect",
"params": {"name": "backup-20260710-120000-manual.zip"}
}
{
"jsonrpc": "2.0",
"id": "backup-inspect-1",
"result": {
"name": "backup-20260710-120000-manual.zip",
"createdAt": "2026-07-10T12:00:00Z",
"size": 1048576,
"status": "success",
"integrity": "unchecked"
}
}
settings.backup.delete
Access: Localhost or admin.
Delete a local backup ZIP.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Local backup filename from settings.backup.list. |
Result
Returns null on success.
Example
{
"jsonrpc": "2.0",
"id": "backup-delete-1",
"method": "settings.backup.delete",
"params": {"name": "backup-20260710-120000-manual.zip"}
}
{
"jsonrpc": "2.0",
"id": "backup-delete-1",
"result": null
}
settings.backup.restore
Access: Localhost or admin.
Transactionally restore a local backup. Restore is rejected while media is active or launching. Core creates a pre-restore safety backup, writes the response, then restarts.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Local backup filename from settings.backup.list. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| restoredFrom | LocalBackup | Yes | Backup restored after full validation. |
| preRestoreBackup | LocalBackup | No | Safety backup created before mutation. |
Example
{
"jsonrpc": "2.0",
"id": "backup-restore-1",
"method": "settings.backup.restore",
"params": {"name": "backup-20260710-120000-manual.zip"}
}
{
"jsonrpc": "2.0",
"id": "backup-restore-1",
"result": {
"restoredFrom": {
"name": "backup-20260710-120000-manual.zip",
"createdAt": "2026-07-10T12:00:00Z",
"size": 1048576,
"status": "success",
"integrity": "valid"
}
}
}
settings.backup.status
Access: All clients. Localhost and paired admin requests may trigger a background refresh of stale remote availability; response never waits for that network request.
Return current local and remote backup state.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| activeOperation | string | No | Active operation, such as local-create, remote-upload, or remote-restore. |
| activeSince | string | No | Operation start time in RFC 3339 format. |
| local | object | Yes | Local backup status entry. |
| remote | object | Yes | Remote backup status entry. |
Backup status entry object
| Key | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | Yes | Whether backup mode is enabled. |
| lastStatus | string | Yes | never, running, success, partial, or failed. |
| lastBackupSize | number | Yes | Latest backup size in bytes. |
| lastRunAt | string | No | Latest attempt time. |
| lastSuccessAt | string | No | Latest successful run time, including unchanged remote runs. |
| lastSnapshotCreatedAt | string | No | Time remote stored content last changed. |
| lastRunNoChanges | boolean | No | Latest remote run succeeded without creating new snapshot. |
| lastError | string | No | Safe latest failure summary. |
| categories | object | No | Category status map. |
| warnings | BackupWarning[] | No | Files skipped by latest run. |
| skippedFiles | number | No | Number of skipped files. |
| schedule | string | No | Remote schedule: daily, weekly, or manual. |
| linked | boolean | No | Whether device has usable remote credentials. |
| deviceName | string | No | Linked remote device name. |
| linkedAt | string | No | Device link time. |
| availability | string | No | Cached remote service availability. |
| availabilityCheckedAt | string | No | Latest availability check time. |
Example
{
"jsonrpc": "2.0",
"id": "backup-status-1",
"method": "settings.backup.status"
}
{
"jsonrpc": "2.0",
"id": "backup-status-1",
"result": {
"local": {"enabled": true, "lastStatus": "success", "lastBackupSize": 1048576},
"remote": {"enabled": true, "linked": true, "schedule": "daily", "lastStatus": "success", "lastBackupSize": 1048576}
}
}
settings.backup.remote.run
Access: Localhost or admin.
Create a manual remote backup. Manual backups remain available when automatic scheduling is disabled. Upload and scheduling require remote service availability.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| backup | RemoteBackup | Yes | Stored remote snapshot metadata. |
| categories | object | Yes | Uploaded category summaries. |
| uploadedFiles | number | Yes | Files uploaded in this run. |
| dedupedFiles | number | Yes | Files already stored remotely. |
| uploadedPacks | number | Yes | Transfer packs uploaded. |
| uploadedBytes | number | Yes | Bytes uploaded. |
| skippedFiles | number | No | Unsafe, unavailable, or oversized files skipped. |
| warnings | BackupWarning[] | No | Structured skipped-file details. |
| storageUsedBytes | number | No | Remote account storage used. |
| storageQuotaBytes | number | No | Remote account storage quota. |
| noChanges | boolean | No | Server already held identical snapshot; run succeeded without new stored content. |
Example
{
"jsonrpc": "2.0",
"id": "backup-remote-run-1",
"method": "settings.backup.remote.run"
}
{
"jsonrpc": "2.0",
"id": "backup-remote-run-1",
"result": {
"backup": {
"id": "01J2BACKUP",
"backupType": "manual",
"schemaVersion": 1,
"createdAt": "2026-07-10T12:00:00Z",
"sizeBytes": 1048576,
"manifestHash": "sha256:example",
"categories": {}
},
"categories": {},
"uploadedFiles": 4,
"dedupedFiles": 20,
"uploadedPacks": 1,
"uploadedBytes": 8192
}
}
settings.backup.remote.list
Access: Localhost or admin.
List remote backups and account quota. Listing existing backups remains available when uploads are unavailable.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| items | RemoteBackup[] | Yes | Remote backups. IDs are opaque. |
| storageUsedBytes | number | Yes | Remote account storage used. |
| storageQuotaBytes | number | Yes | Remote account storage quota. |
Example
{
"jsonrpc": "2.0",
"id": "backup-remote-list-1",
"method": "settings.backup.remote.list"
}
{
"jsonrpc": "2.0",
"id": "backup-remote-list-1",
"result": {
"items": [],
"storageUsedBytes": 0,
"storageQuotaBytes": 1073741824
}
}
settings.backup.remote.restore
Access: Localhost or admin.
Transactionally restore an opaque remote backup ID. Listing and restoring existing backups remain available when uploads are unavailable. Restore is rejected while media is active or launching. Core writes response, then restarts.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Opaque backup ID from settings.backup.remote.list. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| restoredFrom | RemoteBackup | Yes | Remote backup restored after full validation. |
| preRestoreBackup | LocalBackup | No | Local safety backup created before mutation. |
Example
{
"jsonrpc": "2.0",
"id": "backup-remote-restore-1",
"method": "settings.backup.remote.restore",
"params": {"id": "01J2BACKUP"}
}
{
"jsonrpc": "2.0",
"id": "backup-remote-restore-1",
"result": {
"restoredFrom": {
"id": "01J2BACKUP",
"backupType": "manual",
"schemaVersion": 1,
"createdAt": "2026-07-10T12:00:00Z",
"sizeBytes": 1048576,
"manifestHash": "sha256:example",
"categories": {}
}
}
}
A remote API 401 marks device unlinked until a fresh link succeeds. Archives that fail ZIP header, manifest, hash, schema, or platform-policy validation return an RPC error without backup metadata.
Playtime
playtime
Access: All clients.
Query current playtime session status and usage statistics.
This method returns comprehensive information about the current playtime session, including active game time, cumulative session time, cooldown state, daily usage, and remaining time before limits are reached.
Session States:
reset- No active session, ready to start new sessionactive- Game currently running, time being trackedcooldown- Game stopped but session persists (within session reset timeout)
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| state | string | Yes | Current session state: "reset", "active", or "cooldown". |
| sessionActive | boolean | Yes | Whether a game is currently running. |
| limitsEnabled | boolean | Yes | Whether playtime limits are currently enabled for enforcement. |
| sessionStarted | string | No | ISO 8601 timestamp when current game started. Only present during "active" state. |
| sessionDuration | string | No | Total time in current session (Go duration format). Present during "active" and "cooldown" states. |
| sessionCumulativeTime | string | No | Cumulative time from previous games in session. Present during "active" and "cooldown" states. |
| sessionRemaining | string | No | Time remaining before session limit reached. Only present if session limit is configured. |
| cooldownRemaining | string | No | Time until session auto-resets. Only present during "cooldown" state. |
| dailyUsageToday | string | No | Total playtime accumulated today. Available in all states when data is available. |
| dailyRemaining | string | No | Time remaining before daily limit reached. Available in all states if daily limit is configured. |
| sessionExtension | string | No | Extra time granted to the current session on top of the configured session limit. Omitted when nothing was granted. |
| sessionExtendedUntil | string | No | RFC 3339 timestamp when a session-limit waiver lapses. While set, the session limit is not enforced and sessionRemaining is omitted; the daily limit still applies. |
Note: All duration fields use Go's duration format (e.g., "1h30m45s", "45m", "2h").
sessionRemaining already accounts for any granted extension, so a client showing time left needs no extra arithmetic. sessionExtension is reported separately so a client can show that time was granted rather than silently displaying a larger allowance. See playtime.extend.
Examples
Request
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5e-11ef-9c7b-020304050607",
"method": "playtime"
}
Response (reset state)
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5e-11ef-9c7b-020304050607",
"result": {
"state": "reset",
"sessionActive": false,
"limitsEnabled": true,
"dailyUsageToday": "1h30m0s",
"dailyRemaining": "2h30m0s"
}
}
Response (active game with limits)
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5e-11ef-9c7b-020304050607",
"result": {
"state": "active",
"sessionActive": true,
"limitsEnabled": true,
"sessionStarted": "2025-01-22T14:30:00Z",
"sessionDuration": "45m30s",
"sessionCumulativeTime": "15m",
"sessionRemaining": "14m30s",
"dailyUsageToday": "2h15m30s",
"dailyRemaining": "1h44m30s"
}
}
Response (cooldown state)
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5e-11ef-9c7b-020304050607",
"result": {
"state": "cooldown",
"sessionActive": false,
"limitsEnabled": true,
"sessionDuration": "45m30s",
"sessionCumulativeTime": "45m30s",
"sessionRemaining": "14m30s",
"cooldownRemaining": "12m30s",
"dailyUsageToday": "2h15m30s",
"dailyRemaining": "1h44m30s"
}
}
playtime.extend
Access: playtime.extend capability (localhost, or an authenticated admin client).
Grant extra time to the playtime session currently being limited, without stopping what is playing and without changing any configured limit.
The recipient is never named by the caller: a grant always applies to the profile playtime is being enforced against at that moment, so it cannot be aimed at someone else's session. A duration grant is held against the current session only and is cleared when that session resets — when a different profile becomes active, when the cooldown window expires, or when limits are disabled. A today waiver survives all three because it is day-scoped: it lapses at the next local midnight and nowhere else.
The daily limit is never affected. It remains the hard ceiling in both modes; raising it is a settings change, not a grant.
Modes:
durationadds time to the current session's allowance. It requires a session to extend, so it is accepted duringactiveandcooldownstates but rejected duringreset. Cooldown is the common case: the limit stopped the game and the player is about to relaunch.todaywaives the session limit for the recipient profile until the next local midnight. It is day-scoped rather than session-scoped, so it is accepted in any state, and it is rejected when the system clock is unreliable.
A single duration grant must be between 1 minute and 24 hours, and the total accumulated across one session is capped at 24 hours. A grant that would exceed the cap is rejected rather than reduced, so a caller is never told less time was added than it asked for.
The same grant can also be made by scanning a physical card holding **playtime.extend, authorized by an administrator profile's switch ID rather than by a paired client.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| mode | string | Yes | "duration" or "today". |
| duration | string | No | Time to add, in Go duration format (e.g. "15m", "1h30m"). Required for "duration" mode, ignored for "today". |
| requestId | string | No | Idempotency key. Repeating a request ID reports the original grant instead of adding more time. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| mode | string | Yes | The mode that was applied. |
| replayed | boolean | Yes | True when a repeated requestId matched an earlier grant and no time was added. |
| duration | string | No | Time this grant added. Omitted for "today". |
| expires | string | No | RFC 3339 timestamp when a "today" waiver lapses. Omitted for "duration". |
| sessionExtension | string | No | The session's accumulated extension after this grant. |
| profileId | string | No | Recipient profile. Omitted for the shared profile. |
A successful grant emits playtime.extended. A replayed request granted nothing, so it emits no notification.
Examples
Request
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5e-11ef-9c7b-020304050607",
"method": "playtime.extend",
"params": {
"mode": "duration",
"duration": "15m",
"requestId": "5f2c9a10-1d44-4f8e-9f0b-6d1c2a3b4c5d"
}
}
Response
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5e-11ef-9c7b-020304050607",
"result": {
"mode": "duration",
"duration": "15m0s",
"sessionExtension": "15m0s",
"profileId": "0194e2a1-6c3f-7b21-9d4e-8a5b6c7d8e9f",
"replayed": false
}
}
Response (waiving the session limit for the rest of the day)
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-7a5e-11ef-9c7b-020304050607",
"result": {
"mode": "today",
"expires": "2025-01-23T00:00:00-05:00",
"profileId": "0194e2a1-6c3f-7b21-9d4e-8a5b6c7d8e9f",
"replayed": false
}
}
settings.playtime.limits
Access: All clients.
Get current playtime limit configuration.
Returns all configured playtime limits including daily limits, session limits, session reset timeout, warning intervals, and retention settings.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | Yes | Whether playtime limits are enabled for enforcement. |
| daily | string | No | Daily playtime limit in Go duration format (e.g., "4h"). Omitted if not configured. |
| session | string | No | Per-session playtime limit in Go duration format (e.g., "1h"). Omitted if not configured. |
| sessionReset | string | No | Idle timeout before session auto-resets in Go duration format (e.g., "20m"). "0s" means session never resets. |
| warnings | string[] | Yes | List of time intervals when warnings are sent before limits reached (e.g., ["5m", "2m", "1m"]). Empty array if none. |
| retention | number | No | Number of days to retain playtime history. Omitted if not configured. |
Example
Request
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-7a5e-11ef-9c7b-020304050607",
"method": "settings.playtime.limits"
}
Response
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-7a5e-11ef-9c7b-020304050607",
"result": {
"enabled": true,
"daily": "4h",
"session": "1h",
"sessionReset": "20m",
"warnings": ["5m", "2m", "1m"],
"retention": 30
}
}
settings.playtime.limits.update
Access: Requires settings.write.
Update playtime limit settings.
This method updates one or more playtime limit configuration values in-memory and saves changes to disk. Only provided fields will be updated; omitted fields remain unchanged.
Parameters
An object containing any of the following optional keys:
| Key | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | No | Enable or disable playtime limit enforcement. |
| daily | string | No | Daily playtime limit in Go duration format (e.g., "4h", "2h30m"). Use "0" or "0s" to disable daily limit. |
| session | string | No | Per-session playtime limit in Go duration format (e.g., "1h", "45m"). Use "0" or "0s" to disable session limit. |
| sessionReset | string | No | Idle timeout before session auto-resets in Go duration format (e.g., "20m"). Use "0" or "0s" for sessions that never reset. |
| warnings | string[] | No | List of time intervals for warnings in Go duration format (e.g., ["10m", "5m", "1m"]). Empty array disables warnings. |
| retention | number | No | Number of days to retain playtime history. Use 0 for no retention limit. |
Important: Duration strings must use Go duration format: combinations of hours (h), minutes (m), and seconds (s). Examples: "1h", "30m", "1h30m", "2h15m30s".
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "c3d4e5f6-7a5e-11ef-9c7b-020304050607",
"method": "settings.playtime.limits.update",
"params": {
"enabled": true,
"session": "1h",
"warnings": ["10m", "5m", "2m"]
}
}
Response
{
"jsonrpc": "2.0",
"id": "c3d4e5f6-7a5e-11ef-9c7b-020304050607",
"result": null
}
Decks
A deck is a persistent, ordered list of games and cards the user keeps on the device. It is the one list type Zaparoo syncs with a linked online account, and everything about it works with no account: the ID is minted on the device, and a deck reached through a ZapLink is cached in the same list as a read-only copy.
Every deck has a twelve-character ID drawn from the Crockford base32 alphabet (digits and letters without I, L, O and U). Core stores and shows it lower-case and matches it without regard to case; IDs of eight characters from older decks are also accepted, and may use any digit or letter. The same ID names the deck on the device, in its user:deck:<id> tag, on a card and on the account.
A deck opens as a playlist from ZapScript with **playlist.open:deck://<id> (and playlist.load and playlist.play accept the same argument), from the API with decks.open, or by tapping a ZapLink that serves it. Any ZapLink, on any host, whose ZapScript is a single playlist.open, playlist.play or playlist.load command carrying a JSON playlist is kept on the device as a read-only deck, so it opens offline next time, shows in the deck list and has its games tagged. The copy is known by the link it was fetched from and gets a deck ID minted on this device; nothing in the link or the playlist is used as its ID, so it can never replace or stand in for a deck the user owns. At most 200 decks are kept from links; past that the one fetched longest ago is removed. A deck made on the device opens as the playlist ID deck://<id>, and a deck kept from a link opens as the id its playlist was served with, so reopening the deck that is playing recognises it as the deck already open whichever way it was opened: playlist.open keeps its position, and playlist.play moves it on to the next item. A link the linked account vouches for as the user's own runs as served and is not kept as a copy.
A deck item is either a script (a name and the ZapScript it runs) or a card (an online card by ID, with its scripts and display metadata as pulled). A game added from the local library is stored as a script item: Core composes **launch.title:<system>/<title> with the file's disambiguating tags, so the item names the same game on any device, and keeps the file it was added from as the item's media so this device launches exactly that file. Card and deck metadata are stored as received and returned verbatim.
Every indexed file a deck's game items resolve to carries the tag user:deck:<id>, so a deck can be browsed, searched and picked from with the ordinary tag filters, for example **launch.random:SNES?tags=user:deck:0k3v9x2rq7bm. An item resolves by the file it is linked to, or by its title when it has no linked file on this device or that file is no longer indexed, in which case it is linked to the file it matched. A title match only counts when it is a confident one. When the linked file is still indexed but was not found by the last scan, such as a file on a drive that was unplugged, the item keeps its link and its title match carries the tag until the file is found again. Tags are updated in the background shortly after a deck is created, edited or deleted, so decks.new and decks.update return before they are in place, and a title item is linked once its title has been matched; Core sends decks.changed with updated when it links items. The tags are also rebuilt after every reindex, after media.clean.orphans removes files and after a backup is restored. Deck tags cannot be set through media.tags.update.
Decks work with no account. With Library sync on, the decks this device owns sync with the linked Zaparoo Online account: a deck made on either side appears on the other, an edit on one side is taken by the other, and when both sides edited a deck the changes are merged, with a name or description changed on the device kept. A deck deleted on the account is deleted here. Creating, editing and deleting decks is open to every accepted client, like favorites.
Deck object
| Key | Type | Required | Description |
|---|---|---|---|
| deckId | string | Yes | The deck's ID, lower-case. |
| name | string | Yes | Display name, at most 100 characters. |
| description | string | Yes | Description, at most 1000 characters. Empty when unset. |
| owned | boolean | Yes | True for decks made on this device or its account; false for a cached copy of somebody else's deck, which cannot be edited. |
| locked | boolean | Yes | True when the linked Zaparoo Online account locked the deck. A locked deck cannot be edited or deleted on the device. |
| itemCount | number | Yes | Number of items in the deck. |
| items | DeckItem[] | No | The deck's members in order, [] when empty. Omitted by decks. |
| metadata | object | No | Display metadata as received from the account, verbatim. |
| createdAt | number | Yes | Unix timestamp of creation. |
| updatedAt | number | Yes | Unix timestamp of the last change. |
Deck item object
| Key | Type | Required | Description |
|---|---|---|---|
| id | number | Yes | Item ID, kept while the item stays in the deck, including when other items are added, removed or moved. Used by removeItemIds. |
| position | number | Yes | 1-based position in the deck. |
| kind | string | Yes | script or card. |
| name | string | Yes | Display name. |
| zapscript | string | No | The ZapScript a script item runs. |
| cardId | string | No | The online card a card item names. |
| scripts | object[] | No | A card item's scripts, each {name, zapscript}. |
| metadata | object | No | A card item's display metadata, verbatim. |
| media | object | No | The local file a game item is linked to: system, path, name, tags and available (whether the path is currently indexed). Absent for cards and for items added elsewhere that have not been resolved on this device. |
Deck item input
Items are supplied to decks.new and decks.update in this shape. A deck holds at most 120 items. In the items of decks.update only, an entry may instead be just {"id": <item id>}, which keeps that existing item unchanged at its place in the new list.
| Key | Type | Required | Description |
|---|---|---|---|
| kind | string | No | media, script or card. Required for every entry except an id entry. |
| id | number | No | In decks.update items only: an item already in the deck to keep. Takes no other fields. |
| mediaId | number | No | For media: the indexed media to add. Cannot be mixed with system/path. |
| system | string | No | For media: system ID for path-based lookup. Required with path. |
| path | string | No | For media: media path. Required with system. |
| name | string | No | Display name. Required for script; overrides the indexed title for media. |
| zapscript | string | No | Required for script. At most 5000 characters. |
| cardId | string | No | Required for card. |
| scripts | object[] | No | For card: the card's scripts, each {name, zapscript}. |
| metadata | object | No | For card: display metadata to keep with the item. |
decks
Access: All clients.
List every deck without its items.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| decks | Deck[] | Yes | List of decks, most recently changed first. |
decks.get
Access: All clients.
Return one deck with its items.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| deckId | string | Yes | The deck ID. |
Result
A Deck with items.
decks.new
Access: All clients.
Create a deck owned by this device. Core mints the ID. A deck holds at most 120 items; how many decks a device holds is not limited.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Display name, at most 100 characters. |
| description | string | No | At most 1000 characters. |
| items | DeckItemInput[] | No | Members in order, at most 120. |
Result
The created Deck with items.
Example
Request
{
"jsonrpc": "2.0",
"id": "5d8f1b6e-7a5d-11ef-9c7b-020304050607",
"method": "decks.new",
"params": {
"name": "Weekend",
"items": [
{"kind": "media", "mediaId": 42},
{"kind": "script", "name": "Something random", "zapscript": "**launch.random:SNES"}
]
}
}
Response
{
"jsonrpc": "2.0",
"id": "5d8f1b6e-7a5d-11ef-9c7b-020304050607",
"result": {
"deckId": "0k3v9x2rq7bm",
"name": "Weekend",
"description": "",
"owned": true,
"locked": false,
"itemCount": 2,
"items": [
{
"id": 1,
"position": 1,
"kind": "script",
"name": "Super Metroid",
"zapscript": "**launch.title:SNES/Super Metroid (region:us)",
"media": {
"system": "SNES",
"path": "/media/fat/games/SNES/Super Metroid (USA).sfc",
"name": "Super Metroid",
"tags": ["region:us"],
"available": true
}
},
{
"id": 2,
"position": 2,
"kind": "script",
"name": "Something random",
"zapscript": "**launch.random:SNES"
}
],
"createdAt": 1726300000,
"updatedAt": 1726300000
}
}
decks.update
Access: All clients.
Edit an owned deck. A cached copy of somebody else's deck, and a deck the account locked, are read-only. Item edits are applied in the order remove, replace, append, and the result must hold at most 120 items. The whole edit is applied at once, to the deck as it is at that moment: if any part fails, nothing changes. A request with no changes returns the deck as it is and sends no notification.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| deckId | string | Yes | The deck ID. |
| name | string | No | New display name. |
| description | string | No | New description. |
| items | DeckItemInput[] | No | Replaces the whole item list, in order. Entries given as {"id": …} keep that existing item and its ID, so a reorder is items listing the current IDs in the new order; any other entry is a new item. An ID the deck does not hold, or one listed twice, is an error. |
| addItems | DeckItemInput[] | No | Items to append. |
| removeItemIds | number[] | No | Item IDs to remove. An ID not in the deck is ignored. |
Result
The updated Deck with items.
decks.delete
Access: All clients.
Delete a deck, owned or cached. A deck the account locked cannot be deleted.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| deckId | string | Yes | The deck ID. |
Result
None.
decks.open
Access: All clients.
Open a deck as the active playlist. This runs **playlist.open:deck://<id>, the same command a card can carry, and returns once it has run, like run.
Before a deck opens it is brought up to date when that is quick: a cached copy of somebody else's deck is fetched again from its link, and a deck that syncs with an online account is pulled. Either way a slow or offline service never delays the open for more than a few seconds, and the local copy opens when the refresh fails. A fetch that brings nothing new costs no write, and an item a fetch leaves unchanged keeps the file it is linked to on this device, so a refreshed deck goes on launching the files the user picked. A game item whose linked file is still indexed launches that exact file; any other game item runs its title launch. A card item runs its script, or opens its scripts as a nested playlist when it has several, the way the card's own link does.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| deckId | string | Yes | The deck ID. |
| slot | string | No | Media slot to open the playlist in, as the playlist commands' slot argument. |
Result
None.
Profiles
Profiles are lightweight runtime identities: named buckets of preferences, limits, and profile-owned data. One profile is active per device at a time, switched via the API or by scanning an NFC card containing the profile's switch ID (**profile:<switchId>). Profile roles (admin or member) are separate from paired-client roles: profile roles identify who may authorize local household management, while client roles describe which remote device may call privileged APIs.
When no personal profile is active the device is on the implicit shared profile — the device as it behaves when nobody is signed in. The shared profile's playtime limits are the global config limits, its history is unattributed, and it owns everything the device did before profiles existed. Deactivating means switching to the shared profile. To stop the shared profile launching media (parking the device until someone identifies themselves), enable the profilesRequireForLaunch setting (see settings).
A profile's switch ID is a bearer credential: presenting it — by scanning the card it is written on, or by sending it over the API — authorizes switching to that profile with no PIN, on every path. Switch IDs are therefore only returned to clients with profiles.manage for card-writing. This includes localhost, paired admins, and legacy unpaired remote clients when encryption is disabled; paired members never see them. The optional 4-8 digit PIN protects the remaining path: switching by profileId picked from the visible profile list. Leaving a profile is always free — PINs gate entry only.
Data swapping. On supported platforms (currently MiSTer), profile-owned platform data follows the active profile. This includes saved progress and supported account-specific settings; exact items depend on the platform and installed integrations. The shared profile continues to use the platform's existing data, and creating profiles does not move that data. Device-owned settings remain shared across profiles.
A swap requested while media is running is deferred until it stops, so the running session keeps the data it launched with. Progress and failures are reported by the profiles.data notification; the profilesSwapData setting turns swapping off. Deleting a profile does not delete its profile-owned platform data.
Administration and trust model. The first profile is created as admin and must have a PIN; later profiles default member. The first paired client is admin; later pairings default member. Sensitive local UIs call profiles.verify, confirm the returned profile has the admin role, then send the ordinary management request. This is a client-side nuisance gate for parental and kiosk controls, not cryptographic request authorization; no unlock session is retained. Admin paired clients use their client capability directly. The last admin profile/client cannot be removed or demoted. Profiles remain a household convenience boundary, comparable to TV parental controls — not OS account security. Anyone with OS access still owns the device, and while service.encryption is off an unpaired remote client retains legacy admin API capability, apart from the capabilities that require an authenticated connection — currently update.apply. Enabling encryption makes paired-client restrictions enforceable.
Profile object
| Key | Type | Required | Description |
|---|---|---|---|
| profileId | string | Yes | Unique identifier of the profile. |
| name | string | Yes | Display name, e.g. "Dad" or "Kid A". |
| role | string | Yes | admin or member. Admin profiles may authorize local management and must have a PIN. |
| switchId | string | No | Word phrase written to profile switch cards, e.g. corn-arm-truck. A bearer credential: presenting it switches to profile with no PIN. Returned only to clients with profiles.manage. |
| hasPin | boolean | Yes | True when the profile has a PIN set. The PIN itself is never returned. |
| limitsEnabled | boolean | No | Playtime limits enabled override. Omitted = inherit the global setting. |
| dailyLimit | string | No | Daily playtime limit override as a duration string (e.g. 2h30m). Omitted = inherit; 0 = unlimited. |
| sessionLimit | string | No | Session playtime limit override as a duration string. Omitted = inherit; 0 = unlimited. |
| lastUsedAt | number | No | Unix timestamp of most recent successful profile activation. Omitted if never activated. |
| createdAt | number | Yes | Unix timestamp of profile creation. |
| lastUpdatedAt | number | Yes | Unix timestamp of last modification. |
profiles
Access: All clients. switchId is returned only to clients with profiles.manage.
List all profiles.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| profiles | Profile[] | Yes | List of profiles. |
profiles.new
Access: Requires profiles.manage.
Create a new profile. Local UIs may use profiles.verify as a nuisance gate. The switch ID is generated automatically; write it to a card as **profile:<switchId>.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Display name. |
| role | string | No | admin or member; later profiles default member. First profile is always admin. |
| pin | string | No | Optional 4-8 digit PIN required to switch by profileId; mandatory for admin profiles. |
| limitsEnabled | boolean | No | Playtime limits enabled override. |
| dailyLimit | string | No | Daily limit duration override. |
| sessionLimit | string | No | Session limit duration override. |
Result
The created profile object.
profiles.update
Access: Requires profiles.manage.
Update a profile. Local UIs may gate this action with profiles.verify. Migrated profiles without an administrator may still be recovered locally. Omitted fields are unchanged. If the updated profile is currently active, its limit changes apply immediately (without resetting the running session).
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| profileId | string | Yes | Profile to update. |
| name | string | No | New display name. |
| role | string | No | Change between admin and member; final admin cannot be demoted. |
| pin | string | No | Set or replace the PIN; admin profiles must retain one. |
| clearPin | boolean | No | Remove the PIN. |
| limitsEnabled | boolean | No | Playtime limits enabled override. |
| dailyLimit | string | No | Daily limit duration override. |
| sessionLimit | string | No | Session limit duration override. |
| clearLimits | boolean | No | Reset all limit overrides back to inheriting the global config, before any limit fields in the same request are applied. |
| regenerateSwitchId | boolean | No | Issue a new switch ID (lost-card replacement). Old cards stop working. |
Result
The updated profile object.
profiles.delete
Access: Requires profiles.manage.
Delete a profile. Local UIs may gate this action with profiles.verify. The final admin profile cannot be deleted. If it is active, the device switches to the shared profile. Past play history keeps its attribution.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| profileId | string | Yes | Profile to delete. |
Result
Returns null on success.
profiles.active
Access: All clients.
Get the device's currently active profile.
Parameters
None.
Result
The active profile (a subset of the profile object without switchId and timestamps), or null when no profile is active.
profiles.switch
Access: All clients. Profile PIN or switch ID may still be required.
Switch the device's active profile. Switching by profileId requires the profile's PIN when one is set. Switching by switchId never requires a PIN: the switch ID is a bearer credential, and presenting it is equivalent to scanning the profile's card. Calling with neither profileId nor switchId switches to the shared profile (deactivates), which never requires a PIN. Providing both is an error.
If a game is running when the profile changes, its playtime keeps counting against the profile that launched it: switching to another profile starts a fresh limit session for the new person, while deactivating leaves the launch profile's limits in force until the media stops.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| profileId | string | No | Profile to activate, by ID. Requires pin when the profile has one. |
| switchId | string | No | Profile to activate, by switch ID (bearer credential; no PIN needed). |
| pin | string | No | The profile's PIN, for profileId switching. |
Result
The new active profile, or null when deactivated (shared profile).
profiles.verify
Access: All clients. Requires valid profile PIN or switch ID.
Verify a profile credential without switching: either a profile ID plus its PIN, or a switch ID (a bearer credential — resolving it is the verification, same as scanning the card). Success returns the profile's identity and changes nothing on the device: no session, no active-profile change, no server-side grant of any kind. Clients use this to gate their own ad-hoc UI items behind a credential — e.g. a kiosk frontend requiring a parent's PIN before opening its settings screen. The security of whatever the client unlocks is entirely the client's responsibility.
PIN attempts share the same per-profile rate limiter as profiles.switch, so this method cannot be used to brute-force a PIN any faster than switching attempts could.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| profileId | string | No | Profile to verify against. Requires pin when the profile has one. |
| switchId | string | No | Verify by switch ID (bearer credential; no PIN needed). |
| pin | string | No | The profile's PIN, for profileId verification. |
Exactly one of profileId or switchId is required.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| profileId | string | Yes | ID of the verified profile. |
| name | string | Yes | Display name of the verified profile. |
| role | string | Yes | Profile role (admin or member). |
| hasPin | boolean | Yes | Whether the profile has a PIN set. |
Verification failure (wrong PIN, unknown profile or switch ID, rate limited) returns an error, using the same errors as profiles.switch.
Mappings
Mappings are used to modify the contents of tokens before they're launched, based on different types of matching parameters. Stored mappings are queried before every launch and applied to the token if there's a match. This allows, for example, adding ZapScript to a read-only NFC tag based on its UID.
mappings
Access: All clients.
List all mappings.
Returns a list of all active and inactive mappings entries stored on server.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| mappings | Mapping[] | Yes | List of all stored mappings. See mapping object. |
Mapping object
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Internal database ID of mapping entry. Used to reference mapping for updates and deletions. |
| added | string | Yes | Timestamp of the time mapping was created in RFC3339 format. |
| label | string | Yes | An optional display name shown to the user. |
| enabled | boolean | Yes | True if the mapping will be used when looking up matching mappings. |
| type | string | Yes | The field which will be matched against: _ uid: match on UID, if available. UIDs are normalized before matching to remove spaces, colons and convert to lowercase._ text: match on the stored text on token.* data: match on the raw token data, if available. This is converted from bytes to a hexadecimal string and should be matched as this. |
| match | string | Yes | The method used to match a mapping pattern: _ exact: match the entire string exactly to the field._ partial: match part of the string to the field.* regex: use a regular expression to match the field. |
| pattern | string | Yes | Pattern that will be matched against the token, using the above settings. |
| override | string | Yes | Final text that will completely replace the existing token text if a match was successful. |
Example
Request
{
"jsonrpc": "2.0",
"id": "1a8bee28-7aef-11ef-8427-020304050607",
"method": "mappings"
}
Response
{
"jsonrpc": "2.0",
"id": "1a8bee28-7aef-11ef-8427-020304050607",
"result": {
"mappings": [
{
"id": "1",
"added": "1970-01-21T06:08:18+08:00",
"label": "barcode pokemon",
"enabled": true,
"type": "text",
"match": "partial",
"pattern": "9780307468031",
"override": "**launch.search:gbc/*pokemon*gold*"
}
]
}
}
mappings.new
Access: All clients.
Create a new mapping.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| label | string | Yes | An optional display name shown to the user. |
| enabled | boolean | Yes | True if the mapping will be used when looking up matching mappings. |
| type | string | Yes | The field which will be matched against: _ uid: match on UID, if available. UIDs are normalized before matching to remove spaces, colons and convert to lowercase._ text: match on the stored text on token.* data: match on the raw token data, if available. This is converted from bytes to a hexadecimal string and should be matched as this. |
| match | string | Yes | The method used to match a mapping pattern: _ exact: match the entire string exactly to the field._ partial: match part of the string to the field.* regex: use a regular expression to match the field. |
| pattern | string | Yes | Pattern that will be matched against the token, using the above settings. |
| override | string | Yes | Final text that will completely replace the existing token text if a match was successful. At most 8192 bytes. |
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"method": "mappings.new",
"params": {
"label": "Test Mapping",
"enabled": true,
"type": "text",
"match": "exact",
"pattern": "test",
"override": "**launch.system:snes"
}
}
Response
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"result": null
}
mappings.delete
Access: All clients.
Delete an existing mapping.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| id | number | Yes | Database ID of mapping. |
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"method": "mappings.delete",
"params": {
"id": 1
}
}
Response
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"result": null
}
mappings.update
Access: All clients.
Change an existing mapping.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| id | number | Yes | Internal database ID of mapping entry. |
| label | string | No | An optional display name shown to the user. |
| enabled | boolean | No | True if the mapping will be used when looking up matching mappings. |
| type | string | No | The field which will be matched against: _ uid: match on UID, if available. UIDs are normalized before matching to remove spaces, colons and convert to lowercase._ text: match on the stored text on token.* data: match on the raw token data, if available. This is converted from bytes to a hexadecimal string and should be matched as this. |
| match | string | No | The method used to match a mapping pattern: _ exact: match the entire string exactly to the field._ partial: match part of the string to the field.* regex: use a regular expression to match the field. |
| pattern | string | No | Pattern that will be matched against the token, using the above settings. |
| override | string | No | Final text that will completely replace the existing token text if a match was successful. At most 8192 bytes. |
Only keys which are provided in the object will be updated in the database.
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "e98fd686-7e62-11ef-8f8c-020304050607",
"method": "mappings.update",
"params": {
"id": 1,
"enabled": false
}
}
Response
{
"jsonrpc": "2.0",
"id": "e98fd686-7e62-11ef-8f8c-020304050607",
"result": null
}
mappings.reload
Access: All clients.
Reload mappings from the configuration file.
Parameters
None.
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"method": "mappings.reload"
}
Response
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"result": null
}
Readers
readers
Access: All clients.
List all currently connected readers and their capabilities.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| readers | ReaderInfo[] | Yes | A list of all connected readers. |
| holdOwnerReaderId | string | No | ID of the reader whose token is currently tracked as the owner of the running media. Omitted when no token is tracked. |
| holdScanMode | string | No | Effective scan mode of that token, including a #tap or #hold override on the token itself. A tracked owner can be tap: the token still owns the running media, its removal just does not exit. This is resolved exactly as the removal will resolve it, so an owner whose reader has since disconnected reports hold — the decision made while that reader was present still stands. |
Reader info object
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Device path or system identifier of the reader. Legacy field, prefer readerId for stable identification. |
| readerId | string | Yes | Stable reader ID, deterministic across restarts. Format: {driver}-{hash}. |
| driver | string | Yes | Driver type for the reader (e.g., "pn532", "acr122pcsc", "file"). |
| info | string | Yes | Human-readable information about the reader. |
| scanMode | string | Yes | Effective scan mode for this reader ("tap" or "hold"), resolving its [[readers.connect]] entry, then its [readers.drivers.<id>] entry, then the global readers.scan.mode. |
| connected | boolean | Yes | Whether the reader is currently connected. |
| capabilities | string[] | Yes | List of capabilities supported by the reader. |
Example
Request
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"method": "readers"
}
Response
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"result": {
"readers": [
{
"id": "/dev/ttyUSB0",
"readerId": "pn532-ujqixjv6",
"driver": "pn532",
"info": "PN532 (1-2.3.1)",
"scanMode": "tap",
"capabilities": ["read", "write"],
"connected": true
}
],
"holdOwnerReaderId": "pn532-ujqixjv6",
"holdScanMode": "tap"
}
}
readers.write
Access: All clients.
Attempt to write given text to the first available write-capable reader, if possible.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| text | string | Yes | ZapScript to be written to the token. |
| readerId | string | No | ID of a specific reader to write to. If omitted, uses the first available write-capable reader. |
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"method": "readers.write",
"params": {
"text": "**launch.system:snes"
}
}
Response
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"result": null
}
readers.write.cancel
Access: All clients.
Cancel any ongoing write operation.
Parameters
Optionally, an object:
| Key | Type | Required | Description |
|---|---|---|---|
| readerId | string | No | ID of a specific reader to cancel write on. If omitted, cancels on all readers. |
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"method": "readers.write.cancel"
}
Response
{
"jsonrpc": "2.0",
"id": "562c0b60-7ae8-11ef-87d7-020304050607",
"result": null
}
Launchers
launchers
Access: All clients.
List all launchers known to the running service. Suitable for populating a UI launcher picker (for example, when assigning a per-system default via settings.update).
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| systems | string[] | No | Case-insensitive list of system IDs to restrict results to. A missing key or empty list returns every launcher. Values not matching any launcher's system return no launchers for that value, rather than an error, since launcher system IDs can be launchable or virtual systems outside the standard system list. |
| fuzzySystem | boolean | No | Also resolve a system-ID alias to its canonical ID before matching (e.g., "megadrive" matches "Genesis"). Matching is always case-insensitive regardless of this flag. |
The unfiltered response can be large on platforms with many launchers (250+ on MiSTer). Pass systems to scope the request when looking up a specific system's launchers.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| launchers | Launcher[] | Yes | Matching cached launchers, sorted by systemId then id. |
Launcher object
| Key | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Unique launcher identifier. |
| systemId | string | No | The system this launcher targets. Omitted for generic launchers without a fixed system. |
| systemName | string | No | Human-readable system name resolved from system metadata. Omitted when no metadata is available. |
| groups | string[] | No | Group names this launcher belongs to. Group names are valid values for systemDefaults.launcher. |
| available | boolean | Yes | Whether this launcher's runtime dependencies are currently satisfied. |
| availabilityReason | string | No | Why the launcher is unavailable. Omitted when available is true. |
| detected | boolean | No | Whether the platform looked for this launcher and found it installed. Omitted when the platform does not check its launchers, which is every platform that reports nothing here; an omitted value means unknown, not missing. Distinct from available, which is about the launcher's runtime dependencies. |
| default | boolean | No | Whether this launcher is the configured default for its system (systemDefaults.launcher, matched by launcher ID or by any of groups). Omitted (implicitly false) otherwise. |
| backend | string | No | What kind of thing this launcher runs. Currently only mister_core is emitted. Omitted when the platform has nothing to say about this launcher. Clients must ignore backend values they don't recognize. |
| misterCore | MisterCoreInfo | No | Present when backend is mister_core and the core is installed. Absent when the core isn't installed; available and availabilityReason say why. |
MisterCoreInfo object
| Key | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | RBF short name, e.g. "3DO". |
| file | string | Yes | Installed RBF filename, e.g. "3DO_20250101.rbf". Identifies the installed core version. |
| mglPath | string | Yes | SD-relative core identity used to launch it, e.g. "_Console (Dual SDRAM)/3DO". Never an absolute filesystem path. |
Example
Request
{
"jsonrpc": "2.0",
"id": "5b8c3a40-7a5e-11ef-88ff-020304050607",
"method": "launchers",
"params": {
"systems": ["3DO"]
}
}
Response
{
"jsonrpc": "2.0",
"id": "5b8c3a40-7a5e-11ef-88ff-020304050607",
"result": {
"launchers": [
{
"id": "3DO",
"systemId": "3DO",
"systemName": "3DO",
"available": true,
"default": true,
"backend": "mister_core",
"misterCore": {
"name": "3DO",
"file": "3DO_20250101.rbf",
"mglPath": "_Console/3DO"
}
},
{
"id": "DualRAM3DO",
"systemId": "3DO",
"systemName": "3DO",
"available": false,
"availabilityReason": "core not installed: _Console (Dual SDRAM)/3DO",
"backend": "mister_core"
}
]
}
}
launchers.refresh
Access: All clients.
Refresh internal launcher cache, forcing reload of launcher configurations and supported platform launcher dependencies. On MiSTer, this forces an RBF filesystem rescan and rewrites the persisted RBF cache.
Parameters
None.
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "af60e4a0-7a5e-11ef-88ff-020304050607",
"method": "launchers.refresh"
}
Response
{
"jsonrpc": "2.0",
"id": "af60e4a0-7a5e-11ef-88ff-020304050607",
"result": null
}
Service
version
Access: All clients.
Return server's current version and platform.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| platform | string | Yes | ID of platform the service is currently running on. |
| version | string | Yes | Current version of the running Zaparoo service. |
Example
Request
{
"jsonrpc": "2.0",
"id": "ca47f646-7e47-11ef-971a-020304050607",
"method": "version"
}
Response
{
"jsonrpc": "2.0",
"id": "ca47f646-7e47-11ef-971a-020304050607",
"result": {
"platform": "mister",
"version": "2.0.0-dev"
}
}
health
Access: All clients.
Simple health check to verify the server is running and responding.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| status | string | Yes | Health status. Returns "ok" when server is healthy. |
| state | string | Yes | Lifecycle state. Always "ready" here, because this method only answers once the service is fully started. The HTTP /health route reports "starting" and "failed" as well. |
Example
Request
{
"jsonrpc": "2.0",
"id": "db58f757-7e47-11ef-982b-020304050607",
"method": "health"
}
Response
{
"jsonrpc": "2.0",
"id": "db58f757-7e47-11ef-982b-020304050607",
"result": {
"status": "ok",
"state": "ready"
}
}
Inbox
Inbox messages are system notifications stored on the server, typically used to inform the user of events like update availability, errors, or other important information.
inbox
Access: All clients.
List all inbox messages.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| messages | InboxMessage[] | Yes | List of inbox messages. |
Inbox message object
| Key | Type | Required | Description |
|---|---|---|---|
| id | number | Yes | Unique identifier of the message. |
| title | string | Yes | Title of the message. |
| body | string | No | Body text of the message. |
| severity | number | Yes | Severity level (0=info, 1=warning, 2=error). |
| category | string | No | Category of the message. |
| profileId | number | No | Associated profile ID, if applicable. |
| createdAt | string | Yes | Timestamp when message was created in RFC3339 format. |
Example
Request
{
"jsonrpc": "2.0",
"id": "ec69f868-7e47-11ef-993c-020304050607",
"method": "inbox"
}
Response
{
"jsonrpc": "2.0",
"id": "ec69f868-7e47-11ef-993c-020304050607",
"result": {
"messages": [
{
"id": 1,
"title": "Update Available",
"body": "A new version of Zaparoo is available.",
"severity": 0,
"category": "update",
"createdAt": "2024-09-24T17:49:42.938167429+08:00"
}
]
}
}
inbox.delete
Access: All clients.
Delete a specific inbox message by ID.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| id | number | Yes | ID of the message to delete. |
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "fd7a0979-7e47-11ef-9a4d-020304050607",
"method": "inbox.delete",
"params": {
"id": 1
}
}
Response
{
"jsonrpc": "2.0",
"id": "fd7a0979-7e47-11ef-9a4d-020304050607",
"result": null
}
inbox.clear
Access: All clients.
Delete all inbox messages.
Parameters
None.
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "0e8b1a8a-7e48-11ef-9b5e-020304050607",
"method": "inbox.clear"
}
Response
{
"jsonrpc": "2.0",
"id": "0e8b1a8a-7e48-11ef-9b5e-020304050607",
"result": null
}
Clients
clients
Access: Localhost only.
List paired API clients. Pairing secrets and authentication tokens are never returned.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| clients | PairedClient[] | Yes | Paired client metadata. |
Paired client object
| Key | Type | Required | Description |
|---|---|---|---|
| clientId | string | Yes | Opaque client ID. |
| clientName | string | Yes | Name supplied by client during pairing. |
| role | string | Yes | Paired role: admin or member. |
| createdAt | number | Yes | Pairing time as Unix seconds. |
| lastSeenAt | number | Yes | Latest recorded activity as Unix seconds. |
Example
{
"jsonrpc": "2.0",
"id": "clients-list-1",
"method": "clients"
}
{
"jsonrpc": "2.0",
"id": "clients-list-1",
"result": {
"clients": [
{
"clientId": "client-01J2",
"clientName": "Zaparoo App",
"role": "admin",
"createdAt": 1783684800,
"lastSeenAt": 1783688400
}
]
}
}
clients.current
Access: All clients.
Return effective access, pairing status, paired role, and named capabilities for current connection. This method is available to every connection accepted by API transport.
access is one of localhost, member, admin, or legacy. A valid static API key reports access: "admin" with paired: false and role: null. role remains the stored paired role for authenticated paired connections, including paired localhost connections, and is null for unpaired localhost, API-key admin, and legacy. Clients should use capability presence for corresponding UI gates and treat role as display-only. Capability names currently include profiles.manage, settings.write, input, screenshot, and update.apply; the array does not enumerate every callable RPC method.
Parameters
None.
Result
| Key | Type | Description |
|---|---|---|
| access | string | Effective public authority: localhost, member, admin, or legacy. |
| paired | boolean | Whether connection carries an authenticated paired identity. |
| role | string or null | Paired client role, otherwise null. |
| capabilities | array of strings | Effective named capabilities granted to current connection. |
Example
Request
{
"jsonrpc": "2.0",
"id": "1f9a258e-2f86-4bc9-a31b-ec842eb79a42",
"method": "clients.current"
}
Response
{
"jsonrpc": "2.0",
"id": "1f9a258e-2f86-4bc9-a31b-ec842eb79a42",
"result": {
"access": "member",
"paired": true,
"role": "member",
"capabilities": ["input", "screenshot"]
}
}
clients.delete
Access: Localhost only.
Revoke a paired client. Existing encrypted sessions remain active until they disconnect; future sessions cannot authenticate.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| clientId | string | Yes | Opaque client ID from clients. |
Result
Returns null on success.
Example
{
"jsonrpc": "2.0",
"id": "clients-delete-1",
"method": "clients.delete",
"params": {"clientId": "client-01J2"}
}
{
"jsonrpc": "2.0",
"id": "clients-delete-1",
"result": null
}
clients.pair.start
Access: Localhost only.
Start a pairing approval window and return PIN for remote client. First paired client is always assigned admin; later clients default to member when role is omitted.
Parameters
An optional object:
| Key | Type | Required | Description |
|---|---|---|---|
| role | string | No | Role granted after pairing: admin or member. Defaults to member after first client. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| pin | string | Yes | Temporary pairing PIN for remote client. |
| expiresAt | number | Yes | PIN expiration time as Unix seconds. |
See encryption and pairing for remote pairing exchange.
Example
{
"jsonrpc": "2.0",
"id": "clients-pair-start-1",
"method": "clients.pair.start",
"params": {"role": "member"}
}
{
"jsonrpc": "2.0",
"id": "clients-pair-start-1",
"result": {
"pin": "123456",
"expiresAt": 1783685100
}
}
clients.pair.cancel
Access: Localhost only.
Cancel active pairing approval window.
Parameters
None.
Result
Returns null on success.
Example
{
"jsonrpc": "2.0",
"id": "clients-pair-cancel-1",
"method": "clients.pair.cancel"
}
{
"jsonrpc": "2.0",
"id": "clients-pair-cancel-1",
"result": null
}
Remote control
Remote control lets the Zaparoo Online account a device is linked to send a fixed set of commands to it (search and browse the media library, list systems and launchers, launch, stop, run a MiSTer script) through the Zaparoo Online API. It is off until the device owner turns on remoteControlEnabled in settings.update, and is turned off again automatically whenever the account is linked or unlinked. Every command that reaches the device is recorded in a local ledger.
remote.activity
Access: Localhost and authenticated admin clients only.
Return the current remote control status and the most recent entries from the remote command ledger, as an owner-facing record of what the linked account's remote commands have done on this device.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| limit | number | No | Number of entries to return, between 1 and 100. Defaults to 20. |
Result
| Key | Type | Required | Description |
|---|---|---|---|
| status | RemoteStatus | Yes | Why the device is or isn't reachable right now. |
| entries | RemoteActivityEntry[] | Yes | Ledger entries, newest first. |
Remote status object
| Key | Type | Required | Description |
|---|---|---|---|
| state | string | Yes | unknown (nothing reported yet), disabled (remote control is off), unlinked (no linked account), connecting, waiting (polling for commands normally), not_remote_device (the server refused the poll because this device is not the account's designated remote device; choose it on Zaparoo Online), unavailable (the server reports the feature as off), credential_rejected (the server rejected the device credential; link the account again), or error (the last capability heartbeat or poll failed for another reason). |
| lastContactAt | string | No | RFC 3339 time the remote service last answered normally, whether that was a capability heartbeat or a poll. Omitted until the first successful contact. |
| lastErrorCode | string | No | The server's error code for the last failure (for example remote_slot_required), or a short local code such as unreachable. Omitted while the state carries no error. |
Remote activity entry object
| Key | Type | Required | Description |
|---|---|---|---|
| createdAt | string | Yes | RFC 3339 time the command was first received. |
| operationType | string | Yes | The command type, for example launch or media.search. |
| originKind | string | Yes | first_party when the account issued the command directly, or api_key when a User API key did. |
| originKeyName | string | No | Name of the User API key that issued the command. Only present for api_key origins. |
| state | string | Yes | Ledger state: recorded, accepted, executing, terminal (a result was produced), void (the server no longer knew the command), or expired. |
| status | string | No | Outcome reported for a terminal entry: succeeded, failed, or busy. |
| errorCode | string | No | Failure code for a failed entry, for example bad_params, media_not_found, or unsupported. |
Example
Request
{
"jsonrpc": "2.0",
"id": "remote-activity-1",
"method": "remote.activity",
"params": {
"limit": 2
}
}
Response
{
"jsonrpc": "2.0",
"id": "remote-activity-1",
"result": {
"status": {
"state": "waiting",
"lastContactAt": "2026-08-30T01:02:03Z"
},
"entries": [
{
"createdAt": "2026-08-30T01:01:40Z",
"operationType": "launch",
"originKind": "api_key",
"originKeyName": "misterzine",
"state": "terminal",
"status": "succeeded"
},
{
"createdAt": "2026-08-30T01:00:12Z",
"operationType": "media.search",
"originKind": "first_party",
"state": "terminal",
"status": "failed",
"errorCode": "bad_params"
}
]
}
}
Input
Direct platform input control for remote control use cases. These methods bypass the token pipeline entirely: no hooks, history, or sound effects are triggered.
The input macro format is identical to what goes after the : in a ZapScript input.keyboard or input.gamepad command on a token. Each character is a separate keypress, {...} groups are special keys/combos, and \ is the escape character. Macros also support {delay:duration}, {hold:key:duration}, {press:key}, and {release:key}. Press and release have short forms {_key} and {^key}. Delay and explicit hold durations are limited to 30 seconds.
Persistent {press:key} and {release:key} input is available only over supported WebSocket input sessions. A press remains held across requests from that WebSocket until its matching release.
Press, release and hold accept a shifted character ({press:M}, {press:!}), which holds Shift together with the key, and a modifier combo ({press:ctrl+c}, {press:shift+a}). A release matches the press that names the same keys in any order, so {release:shift+m} releases {press:M} and {release:shift+ctrl+a} releases {press:ctrl+shift+a}. Keys are reference counted per WebSocket: a modifier stays down until the last held key that needs it is released. A WebSocket can hold at most 128 distinct key combinations at once. Each WebSocket owns its held keys and buttons; one connection cannot release another connection's input. Core releases all owned input when the WebSocket disconnects, input execution fails, or Core shuts down. HTTP JSON-RPC requests reject persistent press and release tokens because HTTP has no durable session lifecycle.
Keys sent by a client that is neither localhost nor an admin are checked against the allow and block lists in the [zapscript.input] config section, the same lists the ZapScript input commands use. On desktop platforms a default block list rejects keys such as {alt+f4}, {ctrl+alt+delete} and the Linux TTY switches. Wrapping a key in a {press:...}, {release:...} or {hold:...} macro does not evade the lists. Setting block = [] clears the defaults. Unlike the ZapScript path, the API does not apply the mode setting, so plain characters can always be typed by a client that holds input.
On Windows, Core runs unelevated in the user's desktop session, so two limits apply that Core cannot detect or work around: keys sent to a window belonging to an elevated program are silently discarded by Windows, and nothing reaches the secure desktop, meaning UAC prompts, the lock screen and Ctrl+Alt+Del. Keys are injected as scancodes, so which character a key produces still depends on the active keyboard layout.
The virtual gamepad needs a driver on Windows. input.gamepad emulates an Xbox 360 controller through ViGEmBus, a kernel-mode driver the Zaparoo installer offers as an optional task; Core never installs it on its own, and installing it is the only step that needs administrator rights. Gamepad support is also off by default on Windows, because an unexpected virtual pad changes controller numbering in games: set gamepad_enabled = true under [input] to turn it on. With the setting on and the driver absent, input.gamepad reports that the driver is missing instead of reporting success. Uninstalling Zaparoo leaves the driver in place, because other software may be using it; remove it separately through Apps & Features, where it is listed as "ViGEm Bus Driver".
The driver is published for x86 and x64 only, so the ARM64 build of Zaparoo does not offer the task and has no virtual gamepad: gamepad_enabled there reports the driver as missing whatever the setting says. Keyboard input is unaffected on every architecture.
input.keyboard
Access: Requires input.
Press keyboard keys using the ZapScript input macro format.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| keys | string | Yes | Input macro string. Each character is a keypress, {...} for special keys (e.g. {enter}, {f9}, {ctrl+q}). WebSocket requests may use {press:key} and {release:key} to hold a key, shifted character or modifier combo across requests. |
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-1234-5678-9abc-def012345678",
"method": "input.keyboard",
"params": {
"keys": "abc{enter}"
}
}
Response
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-1234-5678-9abc-def012345678",
"result": null
}
input.gamepad
Access: Requires input.
Press gamepad buttons using the ZapScript input macro format.
Parameters
An object:
| Key | Type | Required | Description |
|---|---|---|---|
| buttons | string | Yes | Input macro string. Each character is a button press, {...} for named buttons (e.g. {up}, {start}, {l1}). WebSocket requests may use {press:button} and {release:button} to hold a button across requests. |
Result
Returns null on success.
Example
Request
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-2345-6789-abcd-ef0123456789",
"method": "input.gamepad",
"params": {
"buttons": "^^vv<><>BA{start}"
}
}
Response
{
"jsonrpc": "2.0",
"id": "b2c3d4e5-2345-6789-abcd-ef0123456789",
"result": null
}
Screenshot
screenshot
Access: Requires screenshot.
Capture a screenshot of the current platform display. Returns the image as base64-encoded data and the path where it was saved on disk.
Currently supported on MiSTer and ReplayOS. Other platforms return an error. ReplayOS requires active storage and a loaded libretro core.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| path | string | Yes | Path where the screenshot was saved on disk. |
| data | string | Yes | Base64-encoded image data. |
| size | number | Yes | Size of the image data in bytes. |
Example
Request
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-1234-5678-9abc-def012345678",
"method": "screenshot"
}
Response
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-1234-5678-9abc-def012345678",
"result": {
"path": "/media/fat/screenshots/MiSTer_20260329_181500.png",
"data": "iVBORw0KGgo...",
"size": 245760
}
}
Updates
update.check
Access: Localhost or any paired client.
Check if a newer version of Zaparoo Core is available. Returns version information, release notes, and everything a client needs to decide what to offer: whether the device is eligible for updates at all, whether the release has reached this device yet, and what is currently stopping one being installed.
A check makes the device fetch and verify signed release metadata and write the result to its data directory, which is why it is not open to unpaired remote clients.
On development builds, updateAvailable is always false and eligibility is development.
Parameters
None.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| currentVersion | string | Yes | The currently running version. |
| updateAvailable | boolean | Yes | Whether a newer version is available. |
| autoInstall | boolean | Yes | Whether the device installs updates on its own. Mirrors the updateInstall setting. |
| latestVersion | string | No | The latest available version (if the check succeeded). |
| releaseNotes | string | No | Release notes for the latest version. |
| channel | string | No | The update channel the check used: stable or beta. |
| eligibility | string | No | Whether this install can take OTA updates: eligible, development, unsupported (this install cannot be replaced in place, such as a Windows install under a directory Zaparoo cannot write to), or managed (a package manager owns the install, so it should do the installing). An install that cannot be replaced reports unsupported even when a package manager owns it, because that is the one an install is actually refused for. The exception is a Core embedded in a host application, which reports managed regardless: it never replaces its own executable, so replaceability does not apply. |
| checkedAt | string | No | RFC3339 timestamp of when the release metadata was last fetched. |
| rolloutHeld | boolean | No | The release is newer but has not reached this device's share of the fleet yet. Applying it by hand still works; automatic installs wait. |
| blockedBy | object | No | What is stopping an update being applied right now. Absent when nothing is. |
| deferredReason | string | No | Why an automatic install has been putting this version off. Same values as blockedBy.reason. |
| deferredSince | string | No | RFC3339 timestamp of when this version was first put off. After 24 hours an automatic install goes ahead through the signals that expire. |
| lastResult | object | No | How the previous update finished. Present until a newer result replaces it. |
blockedBy
| Key | Type | Required | Description |
|---|---|---|---|
| reason | string | Yes | Machine-readable reason, from the table below. |
| message | string | Yes | Human-readable explanation, suitable for showing as-is. |
| forceable | boolean | Yes | Whether update.apply with force: true goes ahead anyway. False means the refusal stands whatever is passed. |
Reasons:
| Reason | Forceable | Meaning |
|---|---|---|
| mediaIndexing | No | The media database is being generated. |
| mediaOptimizing | No | The media database is being optimised. |
| mediaScraping | No | Media metadata is being scraped. |
| backupActive | No | A backup, restore or upload is running. |
| readerWriting | No | A reader is part-way through writing a token. |
| restoreActive | No | A restore is holding the databases. |
| activeMedia | Yes | Media is playing and a restart would close it. |
| backgroundMedia | Yes | Media is playing in the background. |
| activePlaylist | Yes | A playlist is running. |
| powerLow | No | The battery is below the level an install needs. |
| powerUnknown | Yes | The battery level could not be read. |
| apiBusy | Yes | The API has not been idle long enough. Automatic installs only. |
blockedBy is what a client should read before offering an update: hide or disable the button when forceable is false, and offer to go ahead when it is true.
lastResult
| Key | Type | Required | Description |
|---|---|---|---|
| at | string | Yes | RFC3339 timestamp of when the update finished. |
| outcome | string | Yes | succeeded, rolledBack (the new build would not start and the old one was put back), rollbackBlocked (the rollback could not be completed), or recoveryRequired. |
| fromVersion | string | No | The version before the update. |
| toVersion | string | No | The version the update was to. |
| detail | string | No | What went wrong, when something did. |
Example
Request
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-1234-5678-9abc-def012345678",
"method": "update.check"
}
Response
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-1234-5678-9abc-def012345678",
"result": {
"currentVersion": "2.9.1",
"latestVersion": "2.10.0",
"updateAvailable": true,
"autoInstall": false,
"releaseNotes": "...",
"channel": "stable",
"eligibility": "eligible",
"checkedAt": "2026-08-18T09:30:00Z",
"blockedBy": {
"reason": "activeMedia",
"message": "media is playing",
"forceable": true
}
}
}
update.apply
Access: Requires update.apply.
Download and apply the latest available update, then gracefully restart the service. The response is sent to the client before the restart occurs.
A Core embedded in a host application refuses update.apply: the host updates Core as part of its own package.
Before anything is downloaded the device checks that it is safe to install: nothing writing to the databases, no backup or token write in progress, nothing playing, and enough battery. A refusal comes back as an error whose message is the same text update.check reports in blockedBy.message. Call update.check first to know in advance, and whether force would get past it.
The battery is checked twice — once before the download and again immediately before the install begins — because a download long enough to matter is also long enough to outlive a charger being unplugged.
This method has no request timeout: the download and install run to completion or unwind on their own. Applying an update is treated as low priority, so it does not delay reader scans or playback control.
While it runs, the device sends update.state notifications.
Parameters
| Key | Type | Required | Description |
|---|---|---|---|
| force | boolean | No | Go ahead through the signals update.check reports as forceable, such as media playing that the restart will close. It does not get past anything that risks data or a device without the power to finish. Defaults to false. |
Parameters may be omitted entirely, which is the same as force: false.
Result
| Key | Type | Required | Description |
|---|---|---|---|
| previousVersion | string | Yes | The version before the update. |
| newVersion | string | Yes | The version after the update. |
Example
Request
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-1234-5678-9abc-def012345678",
"method": "update.apply",
"params": {
"force": true
}
}
Response
{
"jsonrpc": "2.0",
"id": "a1b2c3d4-1234-5678-9abc-def012345678",
"result": {
"previousVersion": "2.9.1",
"newVersion": "2.10.0"
}
}