Reference for the versioned HTTP API and the MCP server: versioning, pagination, bulk endpoints, authentication, errors, identify semantics and the MCP tool catalogue.
The API is versioned by path.
GET /api/v1/*is the full v1 surface. It is permanently frozen and never breaks. All provider proxy routes live here.GET /api/v2/*is additive on top of v1. It carries full v1 parity: all provider proxy routes, the shared public routes, and the reused v1 authenticated handlers (suggestions, users, manual match). On top of that it adds the v2-only endpoints: the keyset list endpoints, search, bulk, stats, and reverse lookups.GET /api/*is a bare alias that mirrors whichever version is configured as the default.
The default version is v1. It is resolved once at boot and stays fixed for the
process lifetime. Override it with PLAYMATCH_DEFAULT_API_VERSION set to v1 or
v2 (case-insensitive). An unset or unparseable value falls back to v1.
Version-scoped responses carry a Playmatch-Api-Version header naming the
resolved version. The same name used as a request header is only a proxy-pinning
hint; the server routes by path and not by this header.
GET /api/versions is unversioned, public, needs no auth, and is cached for an
hour. It returns the default and latest versions, the request_header name,
and a versions array where each entry lists its status, is_default, lifecycle
dates, docs_url and openapi_url.
OpenAPI documents are served per version:
GET /api-docs/v1/openapi.json(servers/api/v1)GET /api-docs/v2/openapi.json(servers/api/v2)GET /api-docs/openapi.jsonaliases the default version
Swagger UI is at /swagger-ui/ with three tabs: v2 (primary), v1, and the alias.
All v2 list endpoints use keyset (cursor) pagination.
limitis clamped to[1, 50]. Absent or0falls back to25. Out-of-range values are clamped, never rejected with a 400.cursoris an opaque base64url string with no padding. Absent or empty starts at page 1.withTotalis honored only on platforms and signature-groups. It is ignored on games, companies and DAT files.
Responses are wrapped in a fixed envelope:
{
"data": [],
"pagination": {
"limit": 25,
"hasNextPage": false,
"hasPreviousPage": false,
"nextCursor": null,
"totalItems": null
}
}hasNextPage is true when there are more rows after the current page.
hasPreviousPage is true when a cursor was supplied, meaning the response is
not the first page. totalItems is only present when withTotal=true is
requested, and only on the bounded reference tables (platforms and
signature-groups) that support it; it is null or absent everywhere else.
Cursor handling has three outcomes. A cursor presented against a changed filter
returns 400 { "code": "cursor_filter_mismatch", "message": ..., "restart": true }.
A malformed cursor returns 400 { "code": "malformed_cursor", ... }. A cursor from
a stale format version silently restarts at page 1.
Eight v2 endpoints accept a batch in one request:
POST /api/v2/games/bulkPOST /api/v2/platforms/bulkPOST /api/v2/companies/bulkPOST /api/v2/signature-groups/bulkPOST /api/v2/dat-files/bulkPOST /api/v2/game-files/bulkPOST /api/v2/identify/bulk/idsPOST /api/v2/identify/bulk/relations
MAX_BULK_ITEMS is 100 per request and the JSON body is capped at 256 KiB. Each
bulk request counts as exactly one request against the per-IP rate limiter, no
matter how many items it carries. A request over the cap returns 400 with an
X-Bulk-Max-Items: 100 header.
Partial failures do not fail the batch. The response stays 200 and reports a
per-item result (ok/notFound for get-by-id, or ok/invalid/error for
identify) for each entry.
Most v2 read endpoints are public. The v2 suggestion endpoint requires a bearer
token with the Automation permission, returning 401 when the token is missing
and 403 when it is insufficient. API keys are HMAC-hashed with API_KEY_PEPPER.
Permission tiers run User < Trusted < Automation < Admin.
Every v2 4xx and 5xx response is a machine-readable JSON envelope:
{ "code": "game_not_found", "message": "game not found" }code is a stable string a client can branch on; message is human-readable.
Some errors carry extra fields: a rejected pagination cursor adds "restart": true,
and a batch over the cap adds "limit" and "received". The envelope is uniform
across the whole v2 surface, including body/query/path validation, auth
(401/403), not-found, the rate-limit 429, and unmatched routes. v1 keeps its
historical plain-text error bodies unchanged.
The per-IP rate limiter's proxy trust settings (TRUSTED_PROXY_CIDRS and
TRUST_CF_CONNECTING_IP) are documented in .env.example.
The identify endpoints match a ROM against the catalogue and return the game it belongs to. Both versions expose the same two routes:
GET .../identify/idsreturns the matched game with its metadata provider ids.GET .../identify/relationsreturns the matched game together with its game files, metadata mappings, signature group, publisher, and company.
Use the v1 paths under /api/v1 or the v2 paths under /api/v2. Pass the file
hashes and, as a fallback, the filename and size as query parameters.
The strongest supplied hash resolves the file. Hashes are tried in order: sha256,
then sha1, then md5, then crc. The crc rung matches on crc and file size together,
since a 32-bit crc alone collides at collection scale, so pass the size alongside a
crc. When no hash matches, the filename and size are tried last. A no-match outcome
is still a 200 with an empty result.
The v2 identify/relations response adds an additionalMatches array. When the
resolving hash is shared by sibling games under other signature providers, those
co-hashed games are listed there, ranked after the primary match. The array is
omitted for single-game hash results and for filename and size matches.
Playmatch ships a Model Context Protocol server so MCP clients can identify ROMs
and browse the catalogue with the same data the public API serves. It is mounted
on the main API listener and speaks MCP over Streamable HTTP at /mcp (so the
endpoint is http://<host>:<PORT>/mcp). It shares the public API per-IP rate
limit.
It is controlled by these environment variables:
MCP_ENABLED- set tofalseto disable the server. Defaults totrue.MCP_PUBLIC_URL- the public origin of the deployment (for examplehttps://playmatch.retrorealm.dev). Used to advertise the endpoint in the discovery document. Leave empty to omit the remote entry.
The server publishes an MCP Server Card at /.well-known/mcp-server-card (and the
same document at /.well-known/mcp.json). A client that already knows the host can
read the card to learn the endpoint and metadata without connecting. This is a
self-hosted card, not a registry listing, so it does not by itself make registries
aware of the server.
The MCP session manager is kept in-process, so the deployment must run as a single instance. This is the same constraint as the in-process cron and import jobs.
The server exposes these 18 tools:
playmatch_identify_rom_by_hash- identify a ROM and return its game id and external metadata provider ids.playmatch_identify_rom_with_relations- identify a ROM and return the game with its platform, company, signature group, DAT file and files.playmatch_get_game- fetch a single game and its external metadata by id.playmatch_get_game_with_relations- fetch a game with its full relations by id.playmatch_get_game_file_history- list the DAT file imports a game file was seen in by game file id.playmatch_list_companies/playmatch_get_company- browse or fetch companies.playmatch_list_platforms/playmatch_get_platform- browse or fetch platforms.playmatch_list_signature_groups/playmatch_get_signature_group- browse or fetch signature groups (DAT publishers such as No-Intro and Redump).playmatch_list_dat_files/playmatch_get_dat_file- browse or fetch DAT files.playmatch_list_dat_file_games- list the games contained in a DAT file.playmatch_get_game_files- fetch the files belonging to a game.playmatch_search_games_by_name- fuzzy game search by human title when you have no hash; returns candidate ids, names, and platforms.playmatch_find_dats_containing_hash- find the DAT files that contain a hash.playmatch_bulk_identify- identify many ROMs in one call (capped at 100 items).