# Multiplayer API — complete reference (api.michitai.com) Cross-platform multiplayer backend for game developers: player accounts, game rooms, matchmaking lobbies, leaderboards, friends/invites, text moderation and realtime updates via REST + official SDKs. Author: Nichita Levandovici (Michitai), Republic of Moldova Contact: support@michitai.com License: proprietary (/LICENSE); SDKs under /SDK-LICENSE. Machine-readable entry points: - OpenAPI 3.0.3 spec: https://api.michitai.com/openapi.json - MCP endpoint (tools over JSON-RPC, streamable HTTP): https://api.michitai.com/mcp - Doc index: https://api.michitai.com/llms.txt - Human docs: https://api.michitai.com/api/index.php ================================================================================ 1. AUTHENTICATION ================================================================================ Header-only; tokens must never appear in URLs. - X-Api-Token: — project key, required on EVERY endpoint - X-Api-Private-Token: — admin key; player list, ban/unban, game data update. Never ship in clients. - X-Game-Player-Token: — per-player token; returned ONCE as "private_key" by game_players.php/register. Client stores it; rotate via /rotate. - X-Realtime-Token: — minted by api/realtime.php/token for the separate realtime (wss/udp) service. Every response is JSON. Errors look like: {"success": false, "error": ""} with HTTP 400/401/403/404/429/500 as appropriate. 429 responses carry {"success": false, "error": "Too many requests", "retry_after": } plus a Retry-After header — honor it. ================================================================================ 2. PLAYERS — /api/game_players.php/ ================================================================================ POST /api/game_players.php/register [ApiToken] Body (guest): {"player_name": "...", "player_data": {...}} Body (Steam): {"steam_ticket": "", "steam_identity": "...", "player_name": "..."} steam_ticket = hex-encoded ISteamUser::GetAuthTicketForWebApi ticket, single-use; steam_identity must equal what the client passed (omit if none). First call creates the account ("account":"created"), later calls log in ("account":"existing"). Requires Steam enabled in cabinet game settings. Response: {"success":true, "player_id":"3", "private_key":"PLAYER_TOKEN", "player_name":"TestPlayer", "player_tag":"#XK4B9P2R", "game_id":2, "auth":"guest"|"steam", "account":"created"|"existing", "steam_id":"7656..." } Player names: 2-50 chars, validated against per-game name rules (api_keys.game_data.name_rules — scripts/case toggles set in the cabinet). PUT /api/game_players.php/login [ApiToken, PlayerToken] Response: {"success":true, "player":{id, game_id, player_name, player_tag, player_data, is_online, last_login, last_heartbeat, last_logout, created_at, updated_at}} POST /api/game_players.php/heartbeat [ApiToken, PlayerToken] Send periodically while the game runs; cron marks stale players offline. Response: {"success":true, "message":"Heartbeat updated", "last_heartbeat":"..."} POST /api/game_players.php/logout [ApiToken, PlayerToken] Response: {"success":true, "message":"...", "last_logout":"..."} PUT /api/game_players.php/rename [ApiToken, PlayerToken] Body: {"new_name": "..."} (2-50 chars, name rules apply) Response: {"success":true, "new_name":"...", "player_id":3} POST /api/game_players.php/rotate [ApiToken, PlayerToken] Issues a new player token, invalidates the current one. Response: {"success":true, "private_key":"NEW_PLAYER_TOKEN", "player_id":3} GET /api/game_players.php/list [ApiToken, PrivateToken] Response: {"success":true, "count":N, "players":[{id, player_name, is_online, last_login, last_logout, last_heartbeat, created_at}]} POST /api/game_players.php/ban [ApiToken, PrivateToken] Body: {"player_id":3, "ban_duration":"hour|day|week|month|quarter|year|forever", "ban_reason":"..."} Response: {"success":true, "ban_id":"...", "player_id":3, "ban_duration":"day", "banned_until":"2026-03-14 09:46:37"|null} POST /api/game_players.php/unban [ApiToken, PrivateToken] Body: {"player_id":3} Response: {"success":true, "player_id":3} ================================================================================ 3. GAME DATA — /api/game_data.php// ================================================================================ GET /api/game_data.php/game/get [ApiToken] Response: {"success":true, "type":"game", "game_id":4, "data":{...}} PUT /api/game_data.php/game/update [ApiToken, PrivateToken] Body: arbitrary JSON object — stored verbatim as the game document. Response: {"success":true, "updated_at":"..."} GET /api/game_data.php/player/get [ApiToken, PlayerToken] Response: {"success":true, "type":"player", "player_id":7, "player_name":"...", "data":{...}} PUT /api/game_data.php/player/update [ApiToken, PlayerToken] Body: arbitrary JSON object — stored verbatim as the player document. Response: {"success":true, "updated_at":"..."} ================================================================================ 4. GAME ROOMS — /api/game_room.php/ ================================================================================ POST /api/game_room.php/create [ApiToken, PlayerToken] Body: {"room_name":"...", "password":"...", "max_players":4, (2-16, default 6) "host_switch":false, "realtime":false, "rules":{...}, "player_data":{...}} host_switch=true: host role transfers when host leaves; false: room deleted. realtime=true: members may mint realtime tokens (realtime.php/token). Fails when the caller is already in a room or matchmaking lobby. Response: {"success":true, "room_id":"<32hex>", "room_name":"...", "join_token":"<16hex>", "is_host":true} join_token is a shareable invite — joining with it bypasses the password. POST /api/game_room.php/list [ApiToken] Body: {"search":"", "limit":20} Response: {"success":true, "rooms":[{room_id, room_name, max_players, current_players, has_password, host_switch, can_leave, realtime, rules}]} POST /api/game_room.php/{roomId}/join [ApiToken, PlayerToken] Body: {"password":"...", "join_token":"...", "player_data":{...}} join_token bypasses the password. GET /api/game_room.php/players [ApiToken, PlayerToken] Roster of the caller's current room. POST /api/game_room.php/leave [ApiToken, PlayerToken] POST /api/game_room.php/heartbeat [ApiToken, PlayerToken] GET /api/game_room.php/current [ApiToken, PlayerToken] Room details; members see join_token for self-serve invites. POST /api/game_room.php/stop [ApiToken, PlayerToken] host only POST /api/game_room.php/kick [ApiToken, PlayerToken] host only Body: {"player_id":48} POST /api/game_room.php/password [ApiToken, PlayerToken] host only Body: {"password":"newpass"} — empty string removes the password. --- Room actions (request → response flow) ------------------------------------ POST /api/game_room.php/actions [ApiToken, PlayerToken] Body: {"target_players":"all|host|others|specific", "target_players_ids":[47,48], (required when "specific") "action_type":"move", "request_data":{...}} Non-host players may only target "host". Response: {"success":true, "actions_sent":3, "action_ids":[...], "target_players_ids":[47,48,49]} GET /api/game_room.php/actions/poll [ApiToken, PlayerToken] Responses to actions the caller sent: {"success":true, "actions":[{action_id, action_type, response_data, status:"completed", target_id, is_host, processed_at}]} GET /api/game_room.php/actions/pending [ApiToken, PlayerToken] Actions addressed to the caller: {"success":true, "actions":[{action_id, player_id, target_id, action_type, request_data, created_at, player_name, is_host}]} POST /api/game_room.php/actions/{actionId}/complete [ApiToken, PlayerToken] Body: {"status":"completed"|"failed", "response_data":{...}} --- Room updates (fire-and-forget broadcast) ---------------------------------- POST /api/game_room.php/updates [ApiToken, PlayerToken] Body: {"target_players":"all|host|others|specific", "target_players_ids":[...], "type":"play_animation", "data":{...}} POST /api/game_room.php/updates/poll [ApiToken, PlayerToken] Body: {"from_players":"all|host|others|specific", (default "host") "from_players_ids":[...], "last_update":""} Response: {"success":true, "updates":[{update_id, from_player_id, type, data, created_at}], "last_update":""} ================================================================================ 5. MATCHMAKING — /api/matchmaking.php/ ================================================================================ Lobbies gather players, then the host calls /start which converts the lobby into a game room (members are moved automatically). POST /api/matchmaking.php/list [ApiToken] Body: {"search":"", "limit":20} Response: {"success":true, "lobbies":[{matchmaking_id, matchmaking_name, max_players, current_players, has_password, realtime_room}]} POST /api/matchmaking.php/create [ApiToken, PlayerToken] Body: {"matchmaking_name":"...", "max_players":4, (2-16, REQUIRED) "strict_full":true, # lobby must be full before /start "join_by_requests":true, # joins go through /request + host approve "host_switch":false, "can_leave_room":true, # carried into the room created by /start "realtime_room":false, # started room supports realtime transport "password":"...", "rules":{...}, "player_data":{...}} Response: {"success":true, "matchmaking_id":"<32hex>", "matchmaking_name":"...", "max_players":4, "join_token":"<16hex>", "is_host":true, ...} POST /api/matchmaking.php/{matchmakingId}/request [ApiToken, PlayerToken] For join_by_requests lobbies. Body: {"player_data":{...}} (optional) Response: {"success":true, "request_id":"...", "status":"pending"} POST /api/matchmaking.php/{requestId}/response [ApiToken, PlayerToken] host only Body: {"action":"approve"|"reject"} GET /api/matchmaking.php/{requestId}/status [ApiToken, PlayerToken] Requester polls own request state. GET /api/matchmaking.php/current [ApiToken, PlayerToken] Caller's lobby; members see join_token. POST /api/matchmaking.php/{matchmakingId}/join [ApiToken, PlayerToken] Body: {"password":"...", "join_token":"...", "player_data":{...}} Rejected on join_by_requests lobbies — use /request. join_token bypasses password and host approval. POST /api/matchmaking.php/leave [ApiToken, PlayerToken] GET /api/matchmaking.php/players [ApiToken, PlayerToken] POST /api/matchmaking.php/heartbeat [ApiToken, PlayerToken] POST /api/matchmaking.php/remove [ApiToken, PlayerToken] host only POST /api/matchmaking.php/start [ApiToken, PlayerToken] host only Converts lobby → game room; strict_full requires a full lobby. POST /api/matchmaking.php/stop [ApiToken, PlayerToken] host only, before start POST /api/matchmaking.php/kick [ApiToken, PlayerToken] host only, before start Body: {"player_id":48} POST /api/matchmaking.php/password [ApiToken, PlayerToken] host only, before start Body: {"password":"..."} — empty string removes it. ================================================================================ 6. LEADERBOARD — /api/leaderboard.php ================================================================================ POST /api/leaderboard.php [ApiToken] Body: {"sort_by":["level"], "limit":10} sort_by: non-empty array of numeric player_data field names; array order defines tie-breaking. limit: 1-100, default 10. Response: {"success":true, "leaderboard":[{rank, player_id, player_name, player_data}], "total":4, "sort_by":["level"], "limit":10} ================================================================================ 7. FRIENDS — /api/friends.php/ [all: ApiToken + PlayerToken] ================================================================================ Every player has a permanent public tag "#XXXXXXXX" (8 chars from digits 2-9 and consonants — no vowels, no 0/1). Lookup accepts it with or without '#'. GET /api/friends.php/list {"success":true, "player_tag":"#XK4B9P2R", "friends":[{player_id, player_name, player_tag, is_online, steam_id, relationship:"friend"}], "incoming":[...], "outgoing":[...]} POST /api/friends.php/lookup Body {"player_tag":"#XK4B9P2R"} → {"success":true, "player":{...}, "relationship":null|"friend"|"incoming"|"outgoing"} POST /api/friends.php/request Body {"player_tag":"..."} or {"player_id":102} Auto-accepts when the target already requested the caller → "accepted". → {"success":true, "status":"pending"|"accepted", "player_id":102} POST /api/friends.php/accept Body {"player_id":103} POST /api/friends.php/decline Body {"player_id":103} POST /api/friends.php/remove Body {"player_id":102} POST /api/friends.php/steam Body {"steam_ids":["76561...", ...]} (max 100) Finds game accounts linked to those SteamIDs — each entry is a FriendInfo suitable for /request. POST /api/friends.php/invite Body {"player_id":102} Invites a friend to the caller's current lobby/room. The invite is the server's authorization: invitee joins without password or host approval. → {"success":true, "invite_id":"...", "status":"pending"} GET /api/friends.php/invites → {"success":true, "invites":[{invite_id, kind:"matchmaking"|"room", name, from_player:{...}}]} POST /api/friends.php/invites/accept Body {"invite_id":"..."} → {"success":true, "status":"accepted", "invite_id":"...", "matchmaking_id":"...", "room_id":""} POST /api/friends.php/invites/decline Body {"invite_id":"..."} ================================================================================ 8. REALTIME — /api/realtime.php/token ================================================================================ POST /api/realtime.php/token [ApiToken, PlayerToken] Player must be in a realtime-enabled room. Returns a single-use-window token (5 min) and where to connect: {"success":true, "token":"", "player_info":{"player_id":47,"player_name":"...","room_id":"...", "is_host":true}, "realtime_server":{"host":"realtime.michitai.com","port":443, "protocol":"wss","udp_port":3000}} ================================================================================ 9. MODERATION — /api/censor.php ================================================================================ POST /api/censor.php [ApiToken] Body: {"text":"..."} (max 1000 chars) → {"success":true, "clean":true|false, "filtered":""} SDKs ship a local hash-scan filter for instant checks; this endpoint is authoritative. Rate limited per game+IP. ================================================================================ 10. SERVER TIME — /api/time.php ================================================================================ GET /api/time.php [ApiToken] → {"success":true, "utc":"2026-03-13T14:01:49+00:00", "timestamp":1773410509, "readable":"2026-03-13 14:01:49 UTC"} GET /api/time.php?utc=+1 (integer hour offset; response gains "offset" object) GET /api/time.php?format=unity (Unity-friendly shape) ================================================================================ 11. CONVENTIONS & COMMON FLOWS ================================================================================ - CORS: open (Access-Control-Allow-Origin: *); JSON everywhere; UTC timestamps. - Unity clients: pass ?format=unity and send JSON payloads as STRINGS — player_data_json, rules_json, request_data_json, response_data_json instead of the nested *_data/rules objects. - Optional flag payloads accept both forms server-side; SDKs handle the conversion transparently. Typical session flow: 1. POST game_players.php/register → store private_key 2. PUT game_players.php/login 3. POST game_players.php/heartbeat (repeat while running) 4. Matchmaking: create/list → join or /request → /start → game room OR room: game_room.php/create|list|join 5. In-room: /actions + /actions/pending + /actions/{id}/complete for request-response; /updates + /updates/poll for broadcasts; realtime.php/token for low-latency transport. 6. POST game_room.php/leave → POST game_players.php/logout Common state errors (all {"success":false,"error":...}): - "You are already in a game room. Leave current room first." - "You are already in a matchmaking lobby" - "You cannot create matchmaking while in a game room. Leave room first." - "This matchmaking lobby requires host approval. Use /request endpoint instead." - "Room is full" / "Matchmaking lobby is full" / "Incorrect password" - "Only the host can ..." / "Only host can ..." - "You are banned[: reason][. Ban expires in X|. This is a permanent ban.]" - 404 "Invalid endpoint" — wrong subpath or wrong HTTP method. ================================================================================ 12. SDKs ================================================================================ - Unity (C#/JsonUtility): https://api.michitai.com/unity/index.php - .NET (System.Text.Json): https://api.michitai.com/dotnet/index.php - Java (Jackson): https://api.michitai.com/java/index.php - C++ (nlohmann::json): https://api.michitai.com/cpp/index.php - Python (dataclass+asyncio): https://api.michitai.com/python/index.php Each SDK bundles a local text filter (instant moderation) and name-rules validation; the server remains authoritative on both.