{"openapi":"3.1.0","info":{"title":"iRefFoos Public HTTP API","version":"1.0.0","description":"Only owner-approved supported operations appear here. Documentation visibility is not authorization. Examples are synthetic. Request execution is disabled; use your own tools and credentials deliberately. Swagger UI can consume this same OpenAPI document."},"servers":[{"url":"/","description":"This iRefFoos server"}],"paths":{"/api/qrcode":{"get":{"operationId":"GET_api_qrcode","summary":"Generate a QR image from a URL","description":"Returns a 300-pixel-wide PNG image, not a JSON body. Save the response to a .png file. url is required.\n\nAuthentication: Unauthenticated; no caller credentials are required.","tags":["Match statistics"],"security":[],"parameters":[{"name":"url","in":"query","description":"Text/URL to encode; no navigation or upstream fetch is performed.","schema":{"type":"string"},"example":"https://example.com/stats/DEMO2345","required":true}],"responses":{"200":{"description":"PNG image bytes (synthetic example is a PNG signature representation, not JSON).","content":{"image/png":{"schema":{"type":"string","format":"binary"},"examples":{"synthetic":{"summary":"Binary PNG response","value":"\\x89PNG\\r\\n\\x1a\\n…"}}}}},"400":{"description":"url parameter required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"url parameter required"}}}}}},"500":{"description":"Failed to generate QR code","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Failed to generate QR code"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request GET \"$BASE_URL/api/qrcode?url=https%3A%2F%2Fexample.com%2Fstats%2FDEMO2345\""}]}},"/api/remote/{command}":{"get":{"operationId":"GET_api_remote_command","summary":"Dispatch a remote command (legacy GET; modifies data)","description":"Dispatches a command to an active remote_control host. Success confirms dispatch, not that the host applied it. Prefer POST: GET also changes state and must not be prefetched. Query deviceId takes precedence over JSON, but conflicting selectors are rejected. score_left/right aliases resolve to score_home/away. zone is accepted only by score-up commands and sends a one-shot goal, not a prepared confirmation. Only deviceId and zone are forwarded from request fields; arbitrary payload, scores or match IDs in JSON are not forwarded by this HTTP handler.\n\nAuthentication: Requires an issued API key via X-API-Key (recommended) or legacy apiKey query. Missing/invalid keys return 401; revoked/expired keys and devices outside a scoped key’s grants return 403. Unrestricted keys can target any device. Alias resolution precedes authorization. Headers keep keys out of query URLs.","tags":["Remote control"],"security":[{"RemoteApiKey":[]},{"LegacyQueryApiKey":[]}],"x-ireffoos-mutates":true,"parameters":[{"name":"command","in":"path","description":"Command to dispatch. Left/right scoring aliases map to home/away.","schema":{"type":"string","enum":["score_home_up","score_home_down","score_away_up","score_away_down","score_home_prepare","score_away_prepare","score_goal_cancel","score_left_up","score_left_down","score_right_up","score_right_down","start_game","end_game","end_game_with_score","end_match","end_match_with_score","new_match","new_match_with_id","toggle_recording","toggle_sharing","toggle_summary","toggle_highlights"]},"example":"score_home_up","required":true},{"name":"deviceId","in":"query","description":"Target device; POST may provide it in JSON instead.","schema":{"type":"string"},"example":"demo-table-01","required":true},{"name":"zone","in":"query","description":"Optional scoring rod, only for score-up commands.","schema":{"type":"string","enum":["2","3","5"]},"example":"3","required":false}],"responses":{"200":{"description":"Command sent to the connected host.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"const":true},"command":{"type":"string"}},"required":["success","command"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"success":true,"command":"score_home_up"}}}}}},"400":{"description":"Invalid command, missing/malformed/conflicting target, or invalid zone.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Missing deviceId"}}}}}},"401":{"description":"Missing or invalid key.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"API key required"}}}}}},"403":{"description":"Revoked/expired key or target not authorized.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"API key is not authorized for this device"}}}}}},"404":{"description":"No active remote_control session for this device","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"No active remote_control session for this device"}}}}}},"410":{"description":"Target session exists but its host is disconnected.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"diag":{"type":"object","properties":{"hostNull":{"type":"boolean"},"readyState":{"anyOf":[{"type":"integer"},{"type":"null"}]},"session":{"type":"string"},"type":{"type":"string"}},"required":["hostNull","readyState","session","type"],"additionalProperties":true}},"required":["error","diag"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Host disconnected","diag":{"hostNull":true,"readyState":null,"session":"DEMO42","type":"remote_control"}}}}}}},"500":{"description":"API-key storage lookup failed; no command is dispatched.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Internal error"}}}}}},"503":{"description":"Device authorization unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Device authorization unavailable"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request GET \"$BASE_URL/api/remote/score_home_up?deviceId=demo-table-01&zone=3\" \\\n  --header \"X-API-Key: $IREF_API_KEY\""}]},"post":{"operationId":"POST_api_remote_command","summary":"Dispatch a remote command","description":"Dispatches a command to an active remote_control host. Success confirms dispatch, not that the host applied it. Prefer POST: GET also changes state and must not be prefetched. Query deviceId takes precedence over JSON, but conflicting selectors are rejected. score_left/right aliases resolve to score_home/away. zone is accepted only by score-up commands and sends a one-shot goal, not a prepared confirmation. Only deviceId and zone are forwarded from request fields; arbitrary payload, scores or match IDs in JSON are not forwarded by this HTTP handler.\n\nAuthentication: Requires an issued API key via X-API-Key (recommended) or legacy apiKey query. Missing/invalid keys return 401; revoked/expired keys and devices outside a scoped key’s grants return 403. Unrestricted keys can target any device. Alias resolution precedes authorization. Headers keep keys out of query URLs.","tags":["Remote control"],"security":[{"RemoteApiKey":[]},{"LegacyQueryApiKey":[]}],"x-ireffoos-mutates":true,"parameters":[{"name":"command","in":"path","description":"Command to dispatch. Left/right scoring aliases map to home/away.","schema":{"type":"string","enum":["score_home_up","score_home_down","score_away_up","score_away_down","score_home_prepare","score_away_prepare","score_goal_cancel","score_left_up","score_left_down","score_right_up","score_right_down","start_game","end_game","end_game_with_score","end_match","end_match_with_score","new_match","new_match_with_id","toggle_recording","toggle_sharing","toggle_summary","toggle_highlights"]},"example":"score_home_up","required":true},{"name":"deviceId","in":"query","description":"Target device; POST may provide it in JSON instead.","schema":{"type":"string"},"example":"demo-table-01","required":false},{"name":"zone","in":"query","description":"Optional scoring rod, only for score-up commands.","schema":{"type":"string","enum":["2","3","5"]},"example":"3","required":false}],"requestBody":{"required":false,"description":"JSON alternative to query fields. Query values take precedence.","content":{"application/json":{"schema":{"type":"object","properties":{"deviceId":{"type":"string"},"zone":{"type":["string","number"],"enum":["2","3","5",2,3,5]}},"required":[],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"deviceId":"demo-table-01","zone":"3"}}}}}},"responses":{"200":{"description":"Command sent to the connected host.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"const":true},"command":{"type":"string"}},"required":["success","command"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"success":true,"command":"score_home_up"}}}}}},"400":{"description":"Invalid command, missing/malformed/conflicting target, or invalid zone.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Missing deviceId"}}}}}},"401":{"description":"Missing or invalid key.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"API key required"}}}}}},"403":{"description":"Revoked/expired key or target not authorized.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"API key is not authorized for this device"}}}}}},"404":{"description":"No active remote_control session for this device","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"No active remote_control session for this device"}}}}}},"410":{"description":"Target session exists but its host is disconnected.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"diag":{"type":"object","properties":{"hostNull":{"type":"boolean"},"readyState":{"anyOf":[{"type":"integer"},{"type":"null"}]},"session":{"type":"string"},"type":{"type":"string"}},"required":["hostNull","readyState","session","type"],"additionalProperties":true}},"required":["error","diag"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Host disconnected","diag":{"hostNull":true,"readyState":null,"session":"DEMO42","type":"remote_control"}}}}}}},"500":{"description":"API-key storage lookup failed; no command is dispatched.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Internal error"}}}}}},"503":{"description":"Device authorization unavailable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Device authorization unavailable"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request POST \"$BASE_URL/api/remote/score_home_up?deviceId=demo-table-01&zone=3\" \\\n  --header \"X-API-Key: $IREF_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\"deviceId\":\"demo-table-01\",\"zone\":\"3\"}'"}]}},"/api/remote/goal":{"get":{"operationId":"getRemoteGoal","tags":["Remote scoring"],"summary":"Send a goal command (legacy GET; modifies data)","description":"Send a one-shot goal command to a connected host. Query values take precedence over POST body values, including invalid values. At least deviceId and side must be supplied in the query or JSON body. This changes match data. A successful response means dispatch only; it is not a delivery or scoring guarantee. Prefer POST; the legacy GET variant also modifies data and must not be prefetched.\n\nAuthentication: Requires an issued API key. Send X-API-Key (recommended), or the legacy apiKey query parameter. Missing/invalid keys return 401; revoked/expired keys return 403. Unrestricted keys can target any device. Device-scoped keys require an explicit grant for the canonical target device; an empty list denies all targets. Aliases resolve before authorization; conflicting query/body targets are rejected. Prefer headers to keep credentials out of URLs.","security":[{"RemoteApiKey":[]},{"LegacyQueryApiKey":[]}],"x-ireffoos-mutates":true,"parameters":[{"name":"deviceId","in":"query","description":"Target device. POST may supply this in JSON instead.","required":true,"schema":{"type":"string","description":"Target device with an active remote_control session."},"example":"demo-table-01"},{"name":"side","in":"query","description":"Scoring side. POST may supply this in JSON instead.","required":true,"schema":{"type":"string","enum":["home","away","left","right","guest","visitor"],"description":"Case-insensitive; surrounding whitespace is trimmed."},"example":"home"},{"name":"zone","in":"query","description":"Optional scoring rod. POST may supply this in JSON instead.","required":false,"schema":{"type":"string","enum":["2","3","5"]},"example":"3"}],"responses":{"200":{"description":"Command dispatched to the host, not confirmation that the host applied it.","content":{"application/json":{"schema":{"type":"object","required":["success","command","side"],"additionalProperties":false,"properties":{"success":{"const":true},"command":{"enum":["score_home_up","score_away_up"]},"side":{"type":"string"}}},"examples":{"synthetic":{"value":{"success":true,"command":"score_home_up","side":"home"}}}}}},"400":{"description":"Missing, malformed or conflicting deviceId selectors, or invalid side or zone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"synthetic":{"value":{"error":"Missing deviceId"}}}}}},"401":{"description":"API key missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"synthetic":{"value":{"error":"API key required"}}}}}},"403":{"description":"API key revoked, expired, or not authorized for the resolved device.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"synthetic":{"value":{"error":"API key is not authorized for this device"}}}}}},"404":{"description":"No active remote_control session exists for the device.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"synthetic":{"value":{"error":"No active remote_control session for this device"}}}}}},"410":{"description":"The session exists but its host is disconnected.","content":{"application/json":{"schema":{"type":"object","required":["error","diag"],"properties":{"error":{"const":"Host disconnected"},"diag":{"type":"object","required":["hostNull","readyState","session","type"],"properties":{"hostNull":{"type":"boolean"},"readyState":{"type":["integer","null"]},"session":{"type":"string"},"type":{"type":"string"}},"additionalProperties":false}},"additionalProperties":false},"examples":{"synthetic":{"value":{"error":"Host disconnected","diag":{"hostNull":true,"readyState":null,"session":"DEMO42","type":"remote_control"}}}}}}},"500":{"description":"API key lookup failed; request is not delivered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"synthetic":{"value":{"error":"Internal error"}}}}}},"503":{"description":"Device authorization lookup unavailable; request is not delivered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"synthetic":{"value":{"error":"Device authorization unavailable"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request GET \"$BASE_URL/api/remote/goal?deviceId=demo-table-01&side=home&zone=3\" \\\n  --header \"X-API-Key: $IREF_API_KEY\""}]},"post":{"operationId":"postRemoteGoal","tags":["Remote scoring"],"summary":"Send a goal command","description":"Send a one-shot goal command to a connected host. Query values take precedence over POST body values, including invalid values. At least deviceId and side must be supplied in the query or JSON body. This changes match data. A successful response means dispatch only; it is not a delivery or scoring guarantee. Prefer POST; the legacy GET variant also modifies data and must not be prefetched.\n\nAuthentication: Requires an issued API key. Send X-API-Key (recommended), or the legacy apiKey query parameter. Missing/invalid keys return 401; revoked/expired keys return 403. Unrestricted keys can target any device. Device-scoped keys require an explicit grant for the canonical target device; an empty list denies all targets. Aliases resolve before authorization; conflicting query/body targets are rejected. Prefer headers to keep credentials out of URLs.","security":[{"RemoteApiKey":[]},{"LegacyQueryApiKey":[]}],"x-ireffoos-mutates":true,"parameters":[{"name":"deviceId","in":"query","description":"Target device. POST may supply this in JSON instead.","required":false,"schema":{"type":"string","description":"Target device with an active remote_control session."},"example":"demo-table-01"},{"name":"side","in":"query","description":"Scoring side. POST may supply this in JSON instead.","required":false,"schema":{"type":"string","enum":["home","away","left","right","guest","visitor"],"description":"Case-insensitive; surrounding whitespace is trimmed."},"example":"home"},{"name":"zone","in":"query","description":"Optional scoring rod. POST may supply this in JSON instead.","required":false,"schema":{"type":"string","enum":["2","3","5"]},"example":"3"}],"requestBody":{"required":false,"description":"JSON alternative to query parameters; query values win. deviceId and side must exist across query/body.","content":{"application/json":{"schema":{"type":"object","properties":{"deviceId":{"type":"string","description":"Target device with an active remote_control session."},"side":{"type":"string","enum":["home","away","left","right","guest","visitor"],"description":"Case-insensitive; surrounding whitespace is trimmed."},"zone":{"type":["string","number"],"enum":["2","3","5",2,3,5],"description":"Optional scoring rod."}},"additionalProperties":true},"examples":{"synthetic":{"value":{"deviceId":"demo-table-01","side":"home","zone":"3"}}}}}},"responses":{"200":{"description":"Command dispatched to the host, not confirmation that the host applied it.","content":{"application/json":{"schema":{"type":"object","required":["success","command","side"],"additionalProperties":false,"properties":{"success":{"const":true},"command":{"enum":["score_home_up","score_away_up"]},"side":{"type":"string"}}},"examples":{"synthetic":{"value":{"success":true,"command":"score_home_up","side":"home"}}}}}},"400":{"description":"Missing, malformed or conflicting deviceId selectors, or invalid side or zone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"synthetic":{"value":{"error":"Missing deviceId"}}}}}},"401":{"description":"API key missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"synthetic":{"value":{"error":"API key required"}}}}}},"403":{"description":"API key revoked, expired, or not authorized for the resolved device.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"synthetic":{"value":{"error":"API key is not authorized for this device"}}}}}},"404":{"description":"No active remote_control session exists for the device.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"synthetic":{"value":{"error":"No active remote_control session for this device"}}}}}},"410":{"description":"The session exists but its host is disconnected.","content":{"application/json":{"schema":{"type":"object","required":["error","diag"],"properties":{"error":{"const":"Host disconnected"},"diag":{"type":"object","required":["hostNull","readyState","session","type"],"properties":{"hostNull":{"type":"boolean"},"readyState":{"type":["integer","null"]},"session":{"type":"string"},"type":{"type":"string"}},"additionalProperties":false}},"additionalProperties":false},"examples":{"synthetic":{"value":{"error":"Host disconnected","diag":{"hostNull":true,"readyState":null,"session":"DEMO42","type":"remote_control"}}}}}}},"500":{"description":"API key lookup failed; request is not delivered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"synthetic":{"value":{"error":"Internal error"}}}}}},"503":{"description":"Device authorization lookup unavailable; request is not delivered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"synthetic":{"value":{"error":"Device authorization unavailable"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request POST \"$BASE_URL/api/remote/goal?deviceId=demo-table-01&side=home&zone=3\" \\\n  --header \"X-API-Key: $IREF_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\"deviceId\":\"demo-table-01\",\"side\":\"home\",\"zone\":\"3\"}'"}]}},"/api/stats":{"get":{"operationId":"GET_api_stats","summary":"List saved matches","description":"Lists matches ordered by status and most recently updated, excluding champion scenarios. Optional filters use AND. Each match includes per-game final scores. Responses contain persisted database rows in snake_case; additional fields may be present. Synthetic examples show representative fields, not real match or player data.\n\nAuthentication: Readable without credentials. An optional X-API-Key attributes usage; a supplied revoked key returns 403. This middleware does not require a key or enforce device grants.","tags":["Match statistics"],"security":[{},{"StatsAttributionKey":[]}],"parameters":[{"name":"device_id","in":"query","description":"Exact device ID filter (snake_case).","schema":{"type":"string"},"example":"demo-table-01","required":false},{"name":"source","in":"query","description":"Exact source filter.","schema":{"type":"string"},"example":"default","required":false}],"responses":{"200":{"description":"Matching matches; an empty array means no results.","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"match_id":{"type":"string"},"device_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"score_left":{"type":"integer"},"score_right":{"type":"integer"},"status":{"type":"string"},"current_game_number":{"type":"integer"},"source":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"games":{"type":"array","items":{"type":"object","properties":{"game_number":{"type":"integer"},"score_left":{"type":"integer"},"score_right":{"type":"integer"}},"required":["game_number","score_left","score_right"],"additionalProperties":true}}},"required":["match_id","device_id","score_left","score_right","status","current_game_number","source","created_at","updated_at"],"additionalProperties":true}}},"required":["matches"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"matches":[{"match_id":"DEMO2345","device_id":"demo-table-01","score_left":2,"score_right":1,"status":"active","current_game_number":1,"source":"default","created_at":"2026-01-15T12:00:00.000Z","updated_at":"2026-01-15T12:00:00.000Z","games":[{"game_number":1,"score_left":2,"score_right":1}]}]}}}}}},"403":{"description":"Authorization rejected.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"API key has been revoked"}}}}}},"500":{"description":"Failed to fetch matches","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Failed to fetch matches"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request GET \"$BASE_URL/api/stats?device_id=demo-table-01&source=default\""}]}},"/api/stats/{matchId}":{"get":{"operationId":"GET_api_stats_matchId","summary":"Get a saved match and its games","description":"Responses contain persisted database rows in snake_case; additional fields may be present. Synthetic examples show representative fields, not real match or player data.\n\nAuthentication: Readable without credentials. An optional X-API-Key attributes usage; a supplied revoked key returns 403. This middleware does not require a key or enforce device grants.","tags":["Match statistics"],"security":[{},{"StatsAttributionKey":[]}],"parameters":[{"name":"matchId","in":"path","description":"Required matchId.","schema":{"type":"string"},"example":"DEMO2345","required":true}],"responses":{"200":{"description":"Match and games ordered by game number.","content":{"application/json":{"schema":{"type":"object","properties":{"match":{"type":"object","properties":{"match_id":{"type":"string"},"device_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"score_left":{"type":"integer"},"score_right":{"type":"integer"},"status":{"type":"string"},"current_game_number":{"type":"integer"},"source":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["match_id","device_id","score_left","score_right","status","current_game_number","source","created_at","updated_at"],"additionalProperties":true},"games":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"match_id":{"type":"string"},"game_number":{"type":"integer"},"stats":{"anyOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"final_score_left":{"type":"integer"},"final_score_right":{"type":"integer"},"duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"finalized_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}},"required":["id","match_id","game_number","stats","final_score_left","final_score_right","duration_ms","created_at","updated_at","started_at","finalized_at"],"additionalProperties":true}}},"required":["match","games"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"match":{"match_id":"DEMO2345","device_id":"demo-table-01","score_left":2,"score_right":1,"status":"active","current_game_number":1,"source":"default","created_at":"2026-01-15T12:00:00.000Z","updated_at":"2026-01-15T12:00:00.000Z"},"games":[{"id":12,"match_id":"DEMO2345","game_number":1,"stats":{"zoneStats":{"left3":{"successes":1,"attempts":2,"avgTime":2.4}}},"final_score_left":2,"final_score_right":1,"duration_ms":120000,"created_at":"2026-01-15T12:00:00.000Z","updated_at":"2026-01-15T12:00:00.000Z","started_at":"2026-01-15T12:00:00.000Z","finalized_at":null}]}}}}}},"403":{"description":"Authorization rejected.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"API key has been revoked"}}}}}},"404":{"description":"Match not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Match not found"}}}}}},"500":{"description":"Failed to fetch match stats","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Failed to fetch match stats"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request GET \"$BASE_URL/api/stats/DEMO2345\""}]}},"/api/stats/{matchId}/{gameNumber}":{"get":{"operationId":"GET_api_stats_matchId_gameNumber","summary":"Get a saved game","description":"Retrieves one saved game by match ID and game number. Responses contain persisted database rows in snake_case; additional fields may be present. Synthetic examples show representative fields, not real match or player data.\n\nAuthentication: Readable without credentials. An optional X-API-Key attributes usage; a supplied revoked key returns 403. This middleware does not require a key or enforce device grants.","tags":["Match statistics"],"security":[{},{"StatsAttributionKey":[]}],"parameters":[{"name":"matchId","in":"path","description":"Required matchId.","schema":{"type":"string"},"example":"DEMO2345","required":true},{"name":"gameNumber","in":"path","description":"Required gameNumber.","schema":{"type":"integer"},"example":1,"required":true}],"responses":{"200":{"description":"Saved game row.","content":{"application/json":{"schema":{"type":"object","properties":{"game":{"type":"object","properties":{"id":{"type":"integer"},"match_id":{"type":"string"},"game_number":{"type":"integer"},"stats":{"anyOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"final_score_left":{"type":"integer"},"final_score_right":{"type":"integer"},"duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"finalized_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}},"required":["id","match_id","game_number","stats","final_score_left","final_score_right","duration_ms","created_at","updated_at","started_at","finalized_at"],"additionalProperties":true}},"required":["game"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"game":{"id":12,"match_id":"DEMO2345","game_number":1,"stats":{"zoneStats":{"left3":{"successes":1,"attempts":2,"avgTime":2.4}}},"final_score_left":2,"final_score_right":1,"duration_ms":120000,"created_at":"2026-01-15T12:00:00.000Z","updated_at":"2026-01-15T12:00:00.000Z","started_at":"2026-01-15T12:00:00.000Z","finalized_at":null}}}}}}},"403":{"description":"Authorization rejected.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"API key has been revoked"}}}}}},"404":{"description":"Game not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Game not found"}}}}}},"500":{"description":"Failed to fetch game stats","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Failed to fetch game stats"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request GET \"$BASE_URL/api/stats/DEMO2345/1\""}]}},"/api/stats/{matchId}/{gameNumber}/possessions":{"get":{"operationId":"GET_api_stats_matchId_gameNumber_possessions","summary":"Get game possessions and computed zone statistics","description":"Returns camelCase events plus aggregates over ALL game events, even when limit narrows the event window. avgTime is a seconds string. matchHasConfirmedGoal checks the entire match, not just this game. No API key middleware; unknown games return empty results.\n\nAuthentication: Unauthenticated; no caller credentials are required.","tags":["Match statistics"],"security":[],"parameters":[{"name":"matchId","in":"path","description":"Required matchId.","schema":{"type":"string"},"example":"DEMO2345","required":true},{"name":"gameNumber","in":"path","description":"Required gameNumber.","schema":{"type":"integer"},"example":1,"required":true},{"name":"limit","in":"query","description":"Optional latest-event window; use a positive integer.","schema":{"type":"integer","minimum":1},"example":20,"required":false}],"responses":{"200":{"description":"Event window and full-game aggregates.","content":{"application/json":{"schema":{"type":"object","properties":{"possessions":{"type":"array","items":{"type":"object","properties":{"zone":{"type":"string"},"success":{"type":"boolean"},"durationMs":{"type":"integer"},"sequence":{"type":"integer"},"timestamp":{"type":"integer"},"subZone":{"anyOf":[{"type":"string"},{"type":"null"}]},"confirmedGoal":{"type":"boolean"},"sourceEventId":{"anyOf":[{"type":"integer"},{"type":"null"}]},"originalSuccess":{"anyOf":[{"type":"boolean"},{"type":"null"}]}},"required":["zone","success","durationMs","sequence","timestamp","subZone","confirmedGoal","sourceEventId"],"additionalProperties":true}},"matchHasConfirmedGoal":{"type":"boolean"},"computedZoneStats":{"type":"object","additionalProperties":{"type":"object","properties":{"successes":{"type":"integer"},"attempts":{"type":"integer"},"avgTime":{"type":"string"},"laneSuccesses":{"type":"integer"},"laneAttempts":{"type":"integer"},"wallSuccesses":{"type":"integer"},"wallAttempts":{"type":"integer"}},"required":["successes","attempts","avgTime","laneSuccesses","laneAttempts","wallSuccesses","wallAttempts"],"additionalProperties":true}}},"required":["possessions","matchHasConfirmedGoal","computedZoneStats"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"possessions":[{"zone":"left3","success":true,"durationMs":2400,"sequence":7,"timestamp":1768478400000,"subZone":null,"confirmedGoal":false,"sourceEventId":null,"originalSuccess":null}],"matchHasConfirmedGoal":false,"computedZoneStats":{"left3":{"successes":1,"attempts":1,"avgTime":"2.4","laneSuccesses":0,"laneAttempts":0,"wallSuccesses":0,"wallAttempts":0}}}}}}}},"500":{"description":"Failed to fetch possession events","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Failed to fetch possession events"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request GET \"$BASE_URL/api/stats/DEMO2345/1/possessions?limit=20\""}]}},"/api/stats/{matchId}/all-possessions":{"get":{"operationId":"GET_api_stats_matchId_all_possessions","summary":"Get possessions across every game","description":"Returns camelCase possession events across a match, including gameNumber. No API key middleware is applied. Unknown matches return an empty array, not 404.\n\nAuthentication: Unauthenticated; no caller credentials are required.","tags":["Match statistics"],"security":[],"parameters":[{"name":"matchId","in":"path","description":"Required matchId.","schema":{"type":"string"},"example":"DEMO2345","required":true}],"responses":{"200":{"description":"All saved possessions.","content":{"application/json":{"schema":{"type":"object","properties":{"possessions":{"type":"array","items":{"type":"object","properties":{"zone":{"type":"string"},"success":{"type":"boolean"},"durationMs":{"type":"integer"},"sequence":{"type":"integer"},"timestamp":{"type":"integer"},"subZone":{"anyOf":[{"type":"string"},{"type":"null"}]},"confirmedGoal":{"type":"boolean"},"sourceEventId":{"anyOf":[{"type":"integer"},{"type":"null"}]},"originalSuccess":{"anyOf":[{"type":"boolean"},{"type":"null"}]},"gameNumber":{"type":"integer"}},"required":["zone","success","durationMs","sequence","timestamp","subZone","confirmedGoal","sourceEventId"],"additionalProperties":true}}},"required":["possessions"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"possessions":[{"zone":"left3","success":true,"durationMs":2400,"sequence":7,"timestamp":1768478400000,"subZone":null,"confirmedGoal":false,"sourceEventId":null,"originalSuccess":null,"gameNumber":1}]}}}}}},"500":{"description":"Failed to fetch all possession events","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Failed to fetch all possession events"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request GET \"$BASE_URL/api/stats/DEMO2345/all-possessions\""}]}},"/api/stats/{matchId}/currentgame":{"get":{"operationId":"GET_api_stats_matchId_currentgame","summary":"Get the highest-numbered saved game","description":"Selects the highest saved game number, which may be finalized. It does not use the match current_game_number field. Responses contain persisted database rows in snake_case; additional fields may be present. Synthetic examples show representative fields, not real match or player data.\n\nAuthentication: Readable without credentials. An optional X-API-Key attributes usage; a supplied revoked key returns 403. This middleware does not require a key or enforce device grants.","tags":["Match statistics"],"security":[{},{"StatsAttributionKey":[]}],"parameters":[{"name":"matchId","in":"path","description":"Required matchId.","schema":{"type":"string"},"example":"DEMO2345","required":true}],"responses":{"200":{"description":"Saved game row.","content":{"application/json":{"schema":{"type":"object","properties":{"game":{"type":"object","properties":{"id":{"type":"integer"},"match_id":{"type":"string"},"game_number":{"type":"integer"},"stats":{"anyOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"final_score_left":{"type":"integer"},"final_score_right":{"type":"integer"},"duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"finalized_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}},"required":["id","match_id","game_number","stats","final_score_left","final_score_right","duration_ms","created_at","updated_at","started_at","finalized_at"],"additionalProperties":true}},"required":["game"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"game":{"id":12,"match_id":"DEMO2345","game_number":1,"stats":{"zoneStats":{"left3":{"successes":1,"attempts":2,"avgTime":2.4}}},"final_score_left":2,"final_score_right":1,"duration_ms":120000,"created_at":"2026-01-15T12:00:00.000Z","updated_at":"2026-01-15T12:00:00.000Z","started_at":"2026-01-15T12:00:00.000Z","finalized_at":null}}}}}}},"403":{"description":"Authorization rejected.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"API key has been revoked"}}}}}},"404":{"description":"No games found for this match","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"No games found for this match"}}}}}},"500":{"description":"Failed to fetch current game","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Failed to fetch current game"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request GET \"$BASE_URL/api/stats/DEMO2345/currentgame\""}]}},"/api/stats/{matchId}/qrcode":{"get":{"operationId":"GET_api_stats_matchId_qrcode","summary":"Generate a match stats QR image","description":"Returns a 400-pixel-wide PNG image, not a JSON body. Save the response to a .png file. Encodes this server’s /stats/{matchId} URL without checking whether the match exists.\n\nAuthentication: Readable without credentials. An optional X-API-Key attributes usage; a supplied revoked key returns 403. This middleware does not require a key or enforce device grants.","tags":["Match statistics"],"security":[{},{"StatsAttributionKey":[]}],"parameters":[{"name":"matchId","in":"path","description":"Required matchId.","schema":{"type":"string"},"example":"DEMO2345","required":true}],"responses":{"200":{"description":"PNG image bytes (synthetic example is a PNG signature representation, not JSON).","content":{"image/png":{"schema":{"type":"string","format":"binary"},"examples":{"synthetic":{"summary":"Binary PNG response","value":"\\x89PNG\\r\\n\\x1a\\n…"}}}}},"403":{"description":"Authorization rejected.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"API key has been revoked"}}}}}},"500":{"description":"Failed to generate QR code","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Failed to generate QR code"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request GET \"$BASE_URL/api/stats/DEMO2345/qrcode\""}]}},"/api/stats/device/{deviceId}/current":{"get":{"operationId":"GET_api_stats_device_deviceId_current","summary":"Get a device’s current match","description":"Chooses the latest active match. last=true falls back to the most recent match of any status when there is no active match. Responses contain persisted database rows in snake_case; additional fields may be present. Synthetic examples show representative fields, not real match or player data.\n\nAuthentication: Readable without credentials. An optional X-API-Key attributes usage; a supplied revoked key returns 403. This middleware does not require a key or enforce device grants.","tags":["Match statistics"],"security":[{},{"StatsAttributionKey":[]}],"parameters":[{"name":"deviceId","in":"path","description":"Required deviceId.","schema":{"type":"string"},"example":"demo-table-01","required":true},{"name":"last","in":"query","description":"Fall back to the latest saved match if no active match exists. Only the exact string true enables this.","schema":{"type":"boolean"},"example":true,"required":false}],"responses":{"200":{"description":"Match and games ordered by game number.","content":{"application/json":{"schema":{"type":"object","properties":{"match":{"type":"object","properties":{"match_id":{"type":"string"},"device_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"score_left":{"type":"integer"},"score_right":{"type":"integer"},"status":{"type":"string"},"current_game_number":{"type":"integer"},"source":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["match_id","device_id","score_left","score_right","status","current_game_number","source","created_at","updated_at"],"additionalProperties":true},"games":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"match_id":{"type":"string"},"game_number":{"type":"integer"},"stats":{"anyOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"final_score_left":{"type":"integer"},"final_score_right":{"type":"integer"},"duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"finalized_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}]}},"required":["id","match_id","game_number","stats","final_score_left","final_score_right","duration_ms","created_at","updated_at","started_at","finalized_at"],"additionalProperties":true}}},"required":["match","games"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"match":{"match_id":"DEMO2345","device_id":"demo-table-01","score_left":2,"score_right":1,"status":"active","current_game_number":1,"source":"default","created_at":"2026-01-15T12:00:00.000Z","updated_at":"2026-01-15T12:00:00.000Z"},"games":[{"id":12,"match_id":"DEMO2345","game_number":1,"stats":{"zoneStats":{"left3":{"successes":1,"attempts":2,"avgTime":2.4}}},"final_score_left":2,"final_score_right":1,"duration_ms":120000,"created_at":"2026-01-15T12:00:00.000Z","updated_at":"2026-01-15T12:00:00.000Z","started_at":"2026-01-15T12:00:00.000Z","finalized_at":null}]}}}}}},"403":{"description":"Authorization rejected.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"API key has been revoked"}}}}}},"404":{"description":"No active match found for this device","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"No active match found for this device"}}}}}},"500":{"description":"Failed to get device current match","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":true},"examples":{"synthetic":{"summary":"Synthetic example","value":{"error":"Failed to get device current match"}}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"# Set BASE_URL to your chosen iRefFoos server. Credential variables are never prefilled.\ncurl --request GET \"$BASE_URL/api/stats/device/demo-table-01/current?last=true\""}]}}},"components":{"securitySchemes":{"RemoteApiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Issued API key. No key is embedded in this reference."},"LegacyQueryApiKey":{"type":"apiKey","in":"query","name":"apiKey","description":"Legacy alternative; prefer X-API-Key to avoid keys in URL logs."},"StatsAttributionKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Optional usage attribution. This does not replace X-Stats-API-Key or host ownership."}},"schemas":{"ApiError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}},"additionalProperties":true}}}}