Multiplayer API Logo

Multiplayer API

Core Cells

Multiplayer API for
Game Developers

A scalable, secure REST API to connect any game to a shared multiplayer backend. Manage users, project keys, JSON game states, matchmaking, and RTS sessions — all from one unified interface.

Powerful Features

Everything you need to build, deploy, and scale multiplayer games

Per-Game API Keys

Each project gets a unique key for secure access and complete data isolation between games.

Shared User Base

Players register once and can access multiple games with a single account.

Multiplayer Logic

Built-in support for matchmaking, real-time game sessions, and player state management.

API Endpoints

Comprehensive REST API for multiplayer game development

Method
Endpoint
Description

Game Players

POST
/php/game_players.php/register
Register new player (requires API key, returns Player private key)
PUT
/php/game_players.php/login
Authenticate player (requires API key, Player private key)
POST
/php/game_players.php/heartbeat
Update player heartbeat (requires API key, Player private key)
POST
/php/game_players.php/logout
Logout player (requires API key, Player private key)
GET
/php/game_players.php/list
List all players (requires API key, API private key)

Game Data

GET
/php/game_data.php/game/get
Get game data (requires API key)
PUT
/php/game_data.php/game/update
Update game data (requires API key, API private key)
GET
/php/game_data.php/player/get
Get player data (requires API key, Player private key)
PUT
/php/game_data.php/player/update
Update player data (requires API key, Player private key)

Leaderboard

POST
/php/leaderboard.php
Get ranked leaderboard (requires API key, filters)

Server Data

GET
/php/time.php
Get server time (requires API key)
GET
/php/time.php?utc=+1
Get server time +1 hour offset (requires API key)
GET
/php/time.php?utc=-2
Get server time -2 hours offset (requires API key)

Matchmaking

GET
/php/matchmaking.php/list
List all available matchmaking lobbies (requires API key)
POST
/php/matchmaking.php/create
Create a new matchmaking lobby (requires API key, Player private key)
POST
/php/matchmaking.php/{ID}/request
Request to join a matchmaking lobby (requires API key, Player private key)
POST
/php/matchmaking.php/{ID}/response
Respond to join request (requires API key, Host player private key)
GET
/php/matchmaking.php/{ID}/status
Check join request status (requires API key, Player private key)
GET
/php/matchmaking.php/current
Get current matchmaking status (requires API key, Player private key)
POST
/php/matchmaking.php/{ID}/join
Join a matchmaking lobby directly (requires API key, Player private key)
POST
/php/matchmaking.php/leave
Leave current matchmaking lobby (requires API key, Player private key)
GET
/php/matchmaking.php/players
List all players in current matchmaking (requires API key, Player private key)
POST
/php/matchmaking.php/heartbeat
Send matchmaking heartbeat (requires API key, Player private key)
POST
/php/matchmaking.php/remove
Remove matchmaking lobby (requires API key, Host player private key)
POST
/php/matchmaking.php/start
Start game from matchmaking (requires API key, Host player private key)

Game Rooms

POST
/php/game_room.php/rooms
Create a new game room (requires API key, Player private key)
GET
/php/game_room.php/rooms
List all available game rooms (requires API key)
POST
/php/game_room.php/rooms/{ID}/join
Join an existing room (requires API key, Player private key)
GET
/php/game_room.php/rooms/players
List all players in current room (requires API key, Player private key)
POST
/php/game_room.php/rooms/leave
Leave the current game room (requires API key, Player private key)
POST
/php/game_room.php/heartbeat
Send player activity heartbeat (requires API key, Player private key)
POST
/php/game_room.php/actions
Submit a new game action (requires API key, Player private key)
GET
/php/game_room.php/actions/poll
Get actions result (requires API key, Player private key)
GET
/php/game_room.php/actions/pending
View all pending actions (requires API key, Host player private key)
POST
/php/game_room.php/actions/{ID}/complete
Complete or reject action (requires API key, Host player private key)
POST
/php/game_room.php/updates
Send updates to players (requires API key, Host player private key)
GET
/php/game_room.php/updates/poll
Poll for updates (requires API key, Player private key)
GET
/php/game_room.php/current
Get current game room status (requires API key, Player private key)

SDK for Developers

Integrate our API into your game with our easy-to-use SDKs

C# / Unity SDK

Seamlessly integrate the API into your Unity projects with our C# SDK. Handles authentication, HTTP requests, JSON parsing, and project key management.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace michitai
{
    /// <summary>
    /// Interface for API responses that have success/error fields.
    /// </summary>
    public interface IApiResponse
    {
        bool Success { get; set; }
        string? Error { get; set; }
    }

    /// <summary>
    /// Provides a client for interacting with the MICHITAI Game API.
    /// Handles authentication, player management, game rooms, matchmaking, and actions.
    /// </summary>
    public class GameSDK
    {
        private readonly string _apiToken;
        private readonly string _apiPrivateToken;
        private readonly string _baseUrl;
        private static readonly HttpClient _http = new HttpClient();
        private ILogger? _logger;

        private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };

        /// <summary>
        /// Matchmaking join request actions.
        /// </summary>
        public enum MatchmakingRequestAction
        {
            Approve,
            Reject
        }

        /// <summary>
        /// Initializes a new instance of the GameSDK class.
        /// </summary>
        /// <param name="apiToken">The public API token for authentication.</param>
        /// <param name="apiPrivateToken">The private API token for privileged operations.</param>
        /// <param name="baseUrl">Base URL of the API (default: https://api.michitai.com/v1/php/).</param>
        public GameSDK(string apiToken, string apiPrivateToken, string baseUrl = "https://api.michitai.com/v1/php/", ILogger? logger = null)
        {
            _apiToken = apiToken;
            _apiPrivateToken = apiPrivateToken;
            _baseUrl = baseUrl.EndsWith("/") ? baseUrl : baseUrl + "/";
            _logger = logger;
        }

        /// <summary>
        /// Constructs a URL for API requests with the base URL, endpoint, and authentication token.
        /// </summary>
        /// <param name="endpoint">The API endpoint path.</param>
        /// <param name="extra">Additional query string parameters (optional).</param>
        /// <returns>Fully constructed URL string with authentication.</returns>
        private string Url(string endpoint, string extra = "")
        {
            return $"{_baseUrl}{endpoint}?api_token={_apiToken}{extra}";
        }

        /// <summary>
        /// Sends an HTTP request to the API and deserializes the JSON response.
        /// </summary>
        /// <typeparam name="T">The type to deserialize the response into.</typeparam>
        /// <param name="method">The HTTP method (GET, POST, PUT, etc.).</param>
        /// <param name="url">The URL to send the request to.</param>
        /// <param name="body">The request body (optional). Will be serialized to JSON if provided.</param>
        /// <returns>A task that represents the asynchronous operation. The task result contains the deserialized response.</returns>
        private async Task<T> Send<T>(HttpMethod method, string url, object? body = null) where T : class
        {
            var req = new HttpRequestMessage(method, url);

            if (body != null)
            {
                string json = JsonSerializer.Serialize(body, _jsonOptions);
                req.Content = new StringContent(json, Encoding.UTF8, "application/json");
            }

            var res = await _http.SendAsync(req);
            string str = await res.Content.ReadAsStringAsync();

            // Log the raw response for debugging
            _logger?.Log($"API Response: {str}");

            T response;

            try
            {
                response = JsonSerializer.Deserialize<T>(str, _jsonOptions)!;
                
                // Check if the response indicates an error
                if (response is IApiResponse apiResponse && !apiResponse.Success)
                {
                    _logger?.Error($"API Error: {str}");
                    throw new ApiException(apiResponse.Error ?? "Unknown API error", str);
                }
            }
            catch(JsonException ex)
            {
                _logger?.Warn($"JSON Deserialization Error: {ex.Message}. Response: {str}");
                
                // Try to parse as error response
                try
                {
                    var errorResponse = JsonSerializer.Deserialize<ErrorResponse>(str, _jsonOptions);
                    if (errorResponse != null && !errorResponse.Success)
                    {
                        throw new ApiException(errorResponse.Error ?? "API error", str);
                    }
                }
                catch
                {
                    // If we can't parse as error response, throw the original JSON exception
                    throw new ApiException($"JSON deserialization failed: {ex.Message}", str);
                }
                
                throw;
            }

            return response;
        }

        /// <summary>
        /// Interface for API responses that have success/error fields.
        /// </summary>
        private interface IApiResponse
        {
            bool Success { get; set; }
            string? Error { get; set; }
        }

        /// <summary>
        /// Registers a new player in the game.
        /// </summary>
        /// <param name="name">Display name for the player.</param>
        /// <param name="playerData">Additional player data as an object (will be serialized to JSON).</param>
        /// <returns>Task containing player registration response with player ID and private key.</returns>
        public Task<PlayerRegisterResponse> RegisterPlayer(string name, object playerData)
        {
            return Send<PlayerRegisterResponse>(
                HttpMethod.Post,
                Url("game_players.php/register"),
                new { player_name = name, player_data = playerData }
            );
        }

        /// <summary>
        /// Authenticates a player using their player token.
        /// </summary>
        /// <param name="playerToken">The player's authentication token.</param>
        /// <returns>Task containing player authentication response with player information.</returns>
        public Task<PlayerAuthResponse> AuthenticatePlayer(string playerToken)
        {
            return Send<PlayerAuthResponse>(
                HttpMethod.Put,
                Url("game_players.php/login", $"&game_player_token={playerToken}")
            );
        }

        /// <summary>
        /// Retrieves a list of all players (requires private API token).
        /// </summary>
        /// <returns>Task containing list of players and total count.</returns>
        public Task<PlayerListResponse> GetAllPlayers()
        {
            return Send<PlayerListResponse>(HttpMethod.Get, Url("game_players.php/list", $"&api_private_token={_apiPrivateToken}"));
        }

        /// <summary>
        /// Sends a heartbeat to keep the player's session alive and update last_heartbeat.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <returns>Task containing the heartbeat response.</returns>
        public Task<PlayerHeartbeatResponse> SendPlayerHeartbeatAsync(string gamePlayerToken)
        {
            return Send<PlayerHeartbeatResponse>(
                HttpMethod.Post,
                Url("game_players.php/heartbeat", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Logs out a player and updates last_logout timestamp. Sets is_active to 0.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <returns>Task containing the logout response.</returns>
        public Task<PlayerLogoutResponse> LogoutPlayerAsync(string gamePlayerToken)
        {
            return Send<PlayerLogoutResponse>(
                HttpMethod.Post,
                Url("game_players.php/logout", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Retrieves game data for the current game.
        /// </summary>
        /// <returns>Task containing the game data response.</returns>
        public Task<GameDataResponse> GetGameData()
        {
            return Send<GameDataResponse>(HttpMethod.Get, Url("game_data.php/game/get"));
        }

        /// <summary>
        /// Updates the game data (requires private API token).
        /// </summary>
        /// <param name="data">The game data to update.</param>
        /// <returns>Task indicating success or failure of the update.</returns>
        public Task<SuccessResponse> UpdateGameData(object data)
        {
            return Send<SuccessResponse>(HttpMethod.Put, Url("game_data.php/game/update", $"&api_private_token={_apiPrivateToken}"), data);
        }

        /// <summary>
        /// Retrieves data for a specific player.
        /// </summary>
        /// <param name="playerToken">The player's authentication token.</param>
        /// <returns>Task containing the player's data.</returns>
        public Task<PlayerDataResponse> GetPlayerData(string playerToken)
        {
            return Send<PlayerDataResponse>(
                HttpMethod.Get,
                Url("game_data.php/player/get", $"&game_player_token={playerToken}")
            );
        }

        /// <summary>
        /// Updates data for a specific player.
        /// </summary>
        /// <param name="playerToken">The player's authentication token.</param>
        /// <param name="data">The data to update for the player.</param>
        /// <returns>Task indicating success or failure of the update.</returns>
        public Task<SuccessResponse> UpdatePlayerData(string playerToken, object data)
        {
            return Send<SuccessResponse>(
                HttpMethod.Put,
                Url("game_data.php/player/update", $"&game_player_token={playerToken}"),
                data
            );
        }

        /// <summary>
        /// Retrieves current server time.
        /// </summary>
        /// <returns>Task containing server time in various formats.</returns>
        public Task<ServerTimeResponse> GetServerTime()
        {
            return Send<ServerTimeResponse>(
                HttpMethod.Get,
                Url("time.php")
            );
        }

        /// <summary>
        /// Retrieves server time with UTC offset adjustment.
        /// </summary>
        /// <param name="utcOffset">The UTC offset in hours (e.g., +1, -2).</param>
        /// <returns>Task containing server time with offset information.</returns>
        public Task<ServerTimeWithOffsetResponse> GetServerTimeWithOffset(int utcOffset)
        {
            return Send<ServerTimeWithOffsetResponse>(
                HttpMethod.Get,
                Url("time.php", $"&utc={utcOffset:+#;-#}")
            );
        }

        /// <summary>
        /// Creates a new game room.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <param name="roomName">Name for the new room.</param>
        /// <param name="password">Optional password for the room.</param>
        /// <param name="maxPlayers">Maximum number of players allowed in the room (default: 4).</param>
        /// <returns>Task containing the room creation response.</returns>
        public Task<RoomCreateResponse> CreateRoomAsync(
            string gamePlayerToken,
            string roomName,
            string? password = null,
            int maxPlayers = 4)
        {
            return Send<RoomCreateResponse>(
                HttpMethod.Post,
                Url("game_room.php/rooms", $"&game_player_token={gamePlayerToken}"),
                new
                {
                    room_name = roomName,
                    password = password,
                    max_players = maxPlayers
                }
            );
        }

        /// <summary>
        /// Retrieves a list of all available game rooms.
        /// </summary>
        /// <returns>Task containing the list of rooms.</returns>
        public Task<RoomListResponse> GetRoomsAsync()
        {
            return Send<RoomListResponse>(
                HttpMethod.Get,
                Url("game_room.php/rooms")
            );
        }

        /// <summary>
        /// Joins an existing game room.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <param name="roomId">ID of the room to join.</param>
        /// <param name="password">Room password if required.</param>
        /// <returns>Task containing the join room response.</returns>
        public Task<RoomJoinResponse> JoinRoomAsync(
            string gamePlayerToken,
            string roomId,
            string? password = null)
        {
            return Send<RoomJoinResponse>(
                HttpMethod.Post,
                Url($"game_room.php/rooms/{roomId}/join", $"&game_player_token={gamePlayerToken}"),
                password != null ? new { password = password } : new { password = "" }
            );
        }

        /// <summary>
        /// Leaves the current room.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <returns>Task indicating success or failure of leaving the room.</returns>
        public Task<RoomLeaveResponse> LeaveRoomAsync(string gamePlayerToken)
        {
            return Send<RoomLeaveResponse>(
                HttpMethod.Post,
                Url("game_room.php/rooms/leave", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Retrieves a list of players in the current room.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <returns>Task containing the list of players in the room.</returns>
        public Task<RoomPlayersResponse> GetRoomPlayersAsync(string gamePlayerToken)
        {
            return Send<RoomPlayersResponse>(
                HttpMethod.Get,
                Url("game_room.php/players", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Sends a heartbeat to keep the player's session alive.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <returns>Task containing the heartbeat response.</returns>
        public Task<HeartbeatResponse> SendHeartbeatAsync(string gamePlayerToken)
        {
            return Send<HeartbeatResponse>(
                HttpMethod.Post,
                Url("game_room.php/heartbeat", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Submits a new game action.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <param name="actionType">Type of the action being submitted.</param>
        /// <param name="requestData">Data associated with the action.</param>
        /// <returns>Task containing the action submission response.</returns>
        public Task<ActionSubmitResponse> SubmitActionAsync(
            string gamePlayerToken,
            string actionType,
            object requestData)
        {
            return Send<ActionSubmitResponse>(
                HttpMethod.Post,
                Url("game_room.php/actions", $"&game_player_token={gamePlayerToken}"),
                new
                {
                    action_type = actionType,
                    request_data = requestData
                }
            );
        }

        /// <summary>
        /// Polls for new actions that need to be processed.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <returns>Task containing any pending actions.</returns>
        public Task<ActionPollResponse> PollActionsAsync(string gamePlayerToken)
        {
            return Send<ActionPollResponse>(
                HttpMethod.Get,
                Url("game_room.php/actions/poll", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Retrieves a list of pending actions for the player.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <returns>Task containing the list of pending actions.</returns>
        public Task<ActionPendingResponse> GetPendingActionsAsync(string gamePlayerToken)
        {
            return Send<ActionPendingResponse>(
                HttpMethod.Get,
                Url("game_room.php/actions/pending", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Marks an action as completed.
        /// </summary>
        /// <param name="actionId">ID of the action to complete.</param>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <param name="request">The completion request details.</param>
        /// <returns>Task indicating success or failure of the completion.</returns>
        public Task<ActionCompleteResponse> CompleteActionAsync(
            string actionId,
            string gamePlayerToken,
            ActionCompleteRequest request)
        {
            return Send<ActionCompleteResponse>(
                HttpMethod.Post,
                Url($"game_room.php/actions/{actionId}/complete", $"&game_player_token={gamePlayerToken}"),
                new
                {
                    status = request.Status,
                    response_data = request.Response_data
                }
            );
        }

        /// <summary>
        /// Sends updates to specific players in the room.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <param name="request">The update request containing target players and data.</param>
        /// <returns>Task containing the update response.</returns>
        public Task<UpdatePlayersResponse> UpdatePlayersAsync(
            string gamePlayerToken,
            UpdatePlayersRequest request)
        {
            return Send<UpdatePlayersResponse>(
                HttpMethod.Post,
                Url("game_room.php/updates", $"&game_player_token={gamePlayerToken}"),
                new
                {
                    targetPlayerIds = request.TargetPlayerIds,
                    type = request.Type,
                    dataJson = request.DataJson
                }
            );
        }

        /// <summary>
        /// Polls for player updates in the room.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <param name="lastUpdateId">Optional last update ID to get updates after this point.</param>
        /// <returns>Task containing the poll updates response.</returns>
        public Task<PollUpdatesResponse> PollUpdatesAsync(
            string gamePlayerToken,
            string? lastUpdateId = null)
        {
            string extra = $"&game_player_token={gamePlayerToken}";
            if (!string.IsNullOrEmpty(lastUpdateId))
            {
                extra += $"&lastUpdateId={lastUpdateId}";
            }
            
            return Send<PollUpdatesResponse>(
                HttpMethod.Get,
                Url("game_room.php/updates/poll", extra)
            );
        }

        /// <summary>
        /// Gets the current room information for the player.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <returns>Task containing the current room response.</returns>
        public Task<CurrentRoomResponse> GetCurrentRoomAsync(string gamePlayerToken)
        {
            return Send<CurrentRoomResponse>(
                HttpMethod.Get,
                Url("game_room.php/current", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Lists all available matchmaking lobbies.
        /// </summary>
        /// <returns>Task containing the list of available matchmaking lobbies.</returns>
        public Task<MatchmakingListResponse> GetMatchmakingLobbiesAsync()
        {
            return Send<MatchmakingListResponse>(
                HttpMethod.Get,
                Url("matchmaking.php/list")
            );
        }

        /// <summary>
        /// Creates a new matchmaking lobby.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <param name="maxPlayers">Maximum number of players allowed (2-16).</param>
        /// <param name="strictFull">Whether the game can only start when the lobby is full.</param>
        /// <param name="joinByRequests">Whether players can only join via host approval.</param>
        /// <param name="extraJsonString">Additional host-defined criteria (rank, level, etc.).</param>
        /// <returns>Task containing the matchmaking lobby creation response.</returns>
        public Task<MatchmakingCreateResponse> CreateMatchmakingLobbyAsync(
            string gamePlayerToken,
            int maxPlayers = 4,
            bool strictFull = false,
            bool joinByRequests = false,
            object? extraJsonString = null)
        {
            return Send<MatchmakingCreateResponse>(
                HttpMethod.Post,
                Url("matchmaking.php/create", $"&game_player_token={gamePlayerToken}"),
                new
                {
                    maxPlayers = maxPlayers,
                    strictFull = strictFull,
                    joinByRequests = joinByRequests,
                    extraJsonString = extraJsonString
                }
            );
        }

        /// <summary>
        /// Requests to join a matchmaking lobby that requires host approval.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <param name="matchmakingId">ID of the matchmaking lobby to request to join.</param>
        /// <returns>Task containing the join request response.</returns>
        public Task<MatchmakingJoinRequestResponse> RequestToJoinMatchmakingAsync(
            string gamePlayerToken,
            string matchmakingId)
        {
            return Send<MatchmakingJoinRequestResponse>(
                HttpMethod.Post,
                Url($"matchmaking.php/{matchmakingId}/request", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Responds to a matchmaking join request (approve or reject).
        /// </summary>
        /// <param name="gamePlayerToken">The host player's authentication token.</param>
        /// <param name="matchmakingId">ID of the matchmaking lobby.</param>
        /// <param name="action">Action to take: Approve or Reject.</param>
        /// <returns>Task containing the response to the join request.</returns>
        public Task<MatchmakingRequestResponse> RespondToJoinRequestAsync(
            string gamePlayerToken,
            string matchmakingId,
            MatchmakingRequestAction action)
        {
            return Send<MatchmakingRequestResponse>(
                HttpMethod.Post,
                Url($"matchmaking.php/{matchmakingId}/response", $"&game_player_token={gamePlayerToken}"),
                new { action = action.ToString().ToLower() }
            );
        }

        /// <summary>
        /// Checks the status of a matchmaking join request.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <param name="requestId">ID of the join request to check.</param>
        /// <returns>Task containing the join request status.</returns>
        public Task<MatchmakingRequestStatusResponse> CheckJoinRequestStatusAsync(
            string gamePlayerToken,
            string requestId)
        {
            return Send<MatchmakingRequestStatusResponse>(
                HttpMethod.Get,
                Url($"matchmaking.php/{requestId}/status", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Gets the current player's matchmaking status and lobby information.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <returns>Task containing the current matchmaking status.</returns>
        public Task<MatchmakingCurrentResponse> GetCurrentMatchmakingStatusAsync(string gamePlayerToken)
        {
            return Send<MatchmakingCurrentResponse>(
                HttpMethod.Get,
                Url("matchmaking.php/current", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Joins a matchmaking lobby directly (only works if lobby doesn't require approval).
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <param name="matchmakingId">ID of the matchmaking lobby to join.</param>
        /// <returns>Task containing the direct join response.</returns>
        public Task<MatchmakingDirectJoinResponse> JoinMatchmakingDirectlyAsync(
            string gamePlayerToken,
            string matchmakingId)
        {
            return Send<MatchmakingDirectJoinResponse>(
                HttpMethod.Post,
                Url($"matchmaking.php/{matchmakingId}/join", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Leaves the current matchmaking lobby.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <returns>Task indicating success or failure of leaving the matchmaking lobby.</returns>
        public Task<MatchmakingLeaveResponse> LeaveMatchmakingAsync(string gamePlayerToken)
        {
            return Send<MatchmakingLeaveResponse>(
                HttpMethod.Post,
                Url("matchmaking.php/leave", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Lists all players in the current matchmaking lobby.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <returns>Task containing the list of players in the matchmaking lobby.</returns>
        public Task<MatchmakingPlayersResponse> GetMatchmakingPlayersAsync(string gamePlayerToken)
        {
            return Send<MatchmakingPlayersResponse>(
                HttpMethod.Get,
                Url("matchmaking.php/players", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Sends a heartbeat to keep the player active in the matchmaking lobby.
        /// </summary>
        /// <param name="gamePlayerToken">The player's authentication token.</param>
        /// <returns>Task containing the heartbeat response.</returns>
        public Task<MatchmakingHeartbeatResponse> SendMatchmakingHeartbeatAsync(string gamePlayerToken)
        {
            return Send<MatchmakingHeartbeatResponse>(
                HttpMethod.Post,
                Url("matchmaking.php/heartbeat", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Removes the matchmaking lobby (host only).
        /// </summary>
        /// <param name="gamePlayerToken">The host player's authentication token.</param>
        /// <returns>Task indicating success or failure of removing the matchmaking lobby.</returns>
        public Task<MatchmakingRemoveResponse> RemoveMatchmakingLobbyAsync(string gamePlayerToken)
        {
            return Send<MatchmakingRemoveResponse>(
                HttpMethod.Post,
                Url("matchmaking.php/remove", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Starts a game from matchmaking lobby (host only).
        /// </summary>
        /// <param name="gamePlayerToken">The host player's authentication token.</param>
        /// <returns>Task containing game start response.</returns>
        public Task<MatchmakingStartResponse> StartGameFromMatchmakingAsync(string gamePlayerToken)
        {
            return Send<MatchmakingStartResponse>(
                HttpMethod.Post,
                Url("matchmaking.php/start", $"&game_player_token={gamePlayerToken}")
            );
        }

        /// <summary>
        /// Gets leaderboard with custom sorting options.
        /// </summary>
        /// <param name="sortBy">Array of field names to sort by (e.g., ["level"], ["level", "score"]).</param>
        /// <param name="limit">Maximum number of players to return (1-1000).</param>
        /// <returns>Task containing leaderboard response with ranked players.</returns>
        public Task<LeaderboardResponse> GetLeaderboardAsync(string[] sortBy, int limit = 10)
        {
            return Send<LeaderboardResponse>(
                HttpMethod.Post,
                Url("leaderboard.php"),
                new { sortBy = sortBy, limit = limit }
            );
        }
    }



    public interface ILogger
    {
        void Log(string message);
        void Warn(string message);
        void Error(string message);
    }



    public class PlayerRegisterResponse
    {
        public bool Success { get; set; }
        public required string Player_id { get; set; }
        public required string Private_key { get; set; }
        public required string Player_name { get; set; }
        public int Game_id { get; set; }
    }

    public class PlayerAuthResponse
    {
        public bool Success { get; set; }
        public required PlayerInfo Player { get; set; }
    }

    public class PlayerListResponse
    {
        public bool Success { get; set; }
        public int Count { get; set; }
        public required List<PlayerShort> Players { get; set; }
    }

    public class PlayerShort
    {
        public int Id { get; set; }
        public required string Player_name { get; set; }
        public int Is_active { get; set; }
        public required string Last_login { get; set; }
        public required string Created_at { get; set; }
    }

    public class PlayerInfo
    {
        public int Id { get; set; }
        public int Game_id { get; set; }
        public required string Player_name { get; set; }
        public required Dictionary<string, object> Player_data { get; set; }
        public int Is_active { get; set; }
        public required string Last_login { get; set; }
        public required string Created_at { get; set; }
        public required string Updated_at { get; set; }
    }

    public class PlayerHeartbeatResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
        public required string Message { get; set; }
        public required string Last_heartbeat { get; set; }
    }

    public class PlayerLogoutResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
        public required string Message { get; set; }
        public required string Last_logout { get; set; }
    }

    public class GameDataResponse
    {
        public bool Success { get; set; }
        public required string Type { get; set; }
        public int Game_id { get; set; }
        public required Dictionary<string, object> Data { get; set; }
    }

    public class PlayerDataResponse
    {
        public bool Success { get; set; }
        public required string Type { get; set; }
        public int Player_id { get; set; }
        public required string Player_name { get; set; }
        public required Dictionary<string, object> Data { get; set; }
    }

    public class SuccessResponse
    {
        public bool Success { get; set; }
        public required string Message { get; set; }
        public required string Updated_at { get; set; }
    }

    public class ServerTimeResponse
    {
        public bool Success { get; set; }
        public required string Utc { get; set; }
        public long Timestamp { get; set; }
        public required string Readable { get; set; }
    }

    public class ServerTimeWithOffsetResponse
    {
        public bool Success { get; set; }
        public required string Utc { get; set; }
        public long Timestamp { get; set; }
        public required string Readable { get; set; }
        public required TimeOffset Offset { get; set; }
    }

    public class TimeOffset
    {
        public int Offset_hours { get; set; }
        public required string Offset_string { get; set; }
        public required string Original_utc { get; set; }
        public long Original_timestamp { get; set; }
    }

    public class RoomCreateResponse
    {
        public bool Success { get; set; }
        public required string Room_id { get; set; }
        public required string Room_name { get; set; }
        public bool Is_host { get; set; }
    }

    public class RoomShort
    {
        public required string Room_id { get; set; }
        public required string Room_name { get; set; }
        public int Max_players { get; set; }
        public int Current_players { get; set; }
        public int Has_password { get; set; }
    }

    public class RoomListResponse
    {
        public bool Success { get; set; }
        public required List<RoomShort> Rooms { get; set; }
    }

    public class RoomJoinResponse
    {
        public bool Success { get; set; }
        public required string Room_id { get; set; }
        public required string Message { get; set; }
    }

    public class RoomPlayer
    {
        public required string Player_id { get; set; }
        public required string Player_name { get; set; }
        public int Is_host { get; set; }
        public int Is_online { get; set; }
        public required string Last_heartbeat { get; set; }
    }

    public class RoomPlayersResponse
    {
        public bool Success { get; set; }
        public required List<RoomPlayer> Players { get; set; }
        public required string Last_updated { get; set; }
    }

    public class RoomLeaveResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
        public required string Message { get; set; }
    }

    public class HeartbeatResponse
    {
        public bool Success { get; set; }
        public required string Status { get; set; }
    }

    public class ActionSubmitResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
        public required string Action_id { get; set; }
        public required string Status { get; set; }
    }

    public class ActionInfo
    {
        public required string Action_id { get; set; }
        public required string Action_type { get; set; }
        public object? Response_data { get; set; }
        public required string Status { get; set; }
    }

    public class ActionPollResponse
    {
        public bool Success { get; set; }
        public required List<ActionInfo> Actions { get; set; }
    }

    public class PendingAction
    {
        public required string Action_id { get; set; }
        public required string Player_id { get; set; }
        public required string Action_type { get; set; }
        public object? Request_data { get; set; }
        public required string Created_at { get; set; }
        public required string Player_name { get; set; }
    }

    public class ActionPendingResponse
    {
        public bool Success { get; set; }
        public required List<PendingAction> Actions { get; set; }
    }

    public class ActionCompleteRequest
    {
        public ActionStatus Status { get; set; }
        public object? Response_data { get; set; }



        public ActionCompleteRequest(ActionStatus status, object? responseData = null)
        {
            Status = status;
            Response_data = responseData;
        }
    }

    public class ActionCompleteResponse
    {
        public bool Success { get; set; }
        public required string Message { get; set; }
    }

    public enum ActionStatus
    {
        Pending,
        Processing,
        Completed,
        Failed,
        Read
    }

    public class UpdatePlayersRequest
    {
        public object? TargetPlayerIds { get; set; }
        public required string Type { get; set; }
        public required object DataJson { get; set; }

        [SetsRequiredMembers]
        public UpdatePlayersRequest(object targetPlayerIds, string type, object dataJson)
        {
            TargetPlayerIds = targetPlayerIds;
            Type = type;
            DataJson = dataJson;
        }
    }

    public class UpdatePlayersResponse
    {
        public bool Success { get; set; }
        public int Updates_sent { get; set; }
        public required List<string> Update_ids { get; set; }
        public required List<string> Target_players { get; set; }
    }

    public class PlayerUpdate
    {
        public required string Update_id { get; set; }
        public required string From_player_id { get; set; }
        public required string Type { get; set; }
        public required object Data_json { get; set; }
        public required string Created_at { get; set; }
    }

    public class PollUpdatesResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
        public required List<PlayerUpdate> Updates { get; set; }
        public required string Last_update_id { get; set; }
    }

    public class CurrentRoomInfo
    {
        public required string Room_id { get; set; }
        public required string Room_name { get; set; }
        public bool Is_host { get; set; }
        public bool Is_online { get; set; }
        public int Max_players { get; set; }
        public int Current_players { get; set; }
        public bool Has_password { get; set; }
        public bool Is_active { get; set; }
        public required string Player_name { get; set; }
        public required string Joined_at { get; set; }
        public required string Last_heartbeat { get; set; }
        public required string Room_created_at { get; set; }
        public required string Room_last_activity { get; set; }
    }

    public class CurrentRoomResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
        public bool In_room { get; set; }
        public CurrentRoomInfo? Room { get; set; }
        public List<object>? Pending_actions { get; set; }
        public List<object>? Pending_updates { get; set; }
    }

    public class MatchmakingListResponse
    {
        public bool Success { get; set; }
        public required List<MatchmakingLobby> Lobbies { get; set; }
    }

    public class MatchmakingLobby
    {
        public required string Matchmaking_id { get; set; }
        public int Host_player_id { get; set; }
        public int Max_players { get; set; }
        public int Strict_full { get; set; }
        public object? Extra_json_string { get; set; }
        public required string Created_at { get; set; }
        public required string Last_heartbeat { get; set; }
        public int Current_players { get; set; }
        public required string Host_name { get; set; }
    }

    public class MatchmakingCreateResponse
    {
        public bool Success { get; set; }
        public required string Matchmaking_id { get; set; }
        public int Max_players { get; set; }
        public bool Strict_full { get; set; }
        public bool Join_by_requests { get; set; }
        public bool Is_host { get; set; }
    }

    public class MatchmakingJoinRequestResponse
    {
        public bool Success { get; set; }
        public required string Request_id { get; set; }
        public required string Message { get; set; }
    }

    public class MatchmakingRequestResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
        public required string Message { get; set; }
        public required string Request_id { get; set; }
        public required string Action { get; set; }
    }

    public class MatchmakingRequestStatusResponse
    {
        public bool Success { get; set; }
        public required MatchmakingRequestInfo Request { get; set; }
    }

    public class MatchmakingRequestInfo
    {
        public required string Request_id { get; set; }
        public required string Matchmaking_id { get; set; }
        public required string Status { get; set; }
        public required string Requested_at { get; set; }
        public string? Responded_at { get; set; }
        public int? Responded_by { get; set; }
        public string? Responder_name { get; set; }
        public bool Join_by_requests { get; set; }
    }

    public class MatchmakingCurrentResponse
    {
        public bool Success { get; set; }
        public bool In_matchmaking { get; set; }
        public MatchmakingInfo? Matchmaking { get; set; }
        public required List<object> Pending_requests { get; set; }
    }

    public class MatchmakingInfo
    {
        public required string Matchmaking_id { get; set; }
        public bool Is_host { get; set; }
        public int Max_players { get; set; }
        public int Current_players { get; set; }
        public bool Strict_full { get; set; }
        public bool Join_by_requests { get; set; }
        public object? Extra_json_string { get; set; }
        public required string Joined_at { get; set; }
        public required string Player_status { get; set; }
        public required string Last_heartbeat { get; set; }
        public required string Lobby_heartbeat { get; set; }
        public bool Is_started { get; set; }
        public object? Started_at { get; set; }
    }

    public class MatchmakingDirectJoinResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
        public required string Message { get; set; }
        public required string Matchmaking_id { get; set; }
    }

    public class MatchmakingLeaveResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
        public required string Message { get; set; }
    }

    public class MatchmakingPlayersResponse
    {
        public bool Success { get; set; }
        public required List<MatchmakingPlayer> Players { get; set; }
    }

    public class MatchmakingPlayer
    {
        public int Player_id { get; set; }
        public required string Joined_at { get; set; }
        public required string Last_heartbeat { get; set; }
        public required string Status { get; set; }
        public required string Player_name { get; set; }
        public int Seconds_since_heartbeat { get; set; }
        public int Is_host { get; set; }
    }

    public class MatchmakingHeartbeatResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
        public string? Status { get; set; }  // Made optional for error responses
    }

    public class MatchmakingRemoveResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
        public required string Message { get; set; }
    }

    public class MatchmakingStartResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
        public required string Room_id { get; set; }
        public required string Room_name { get; set; }
        public int Players_transferred { get; set; }
        public required string Message { get; set; }
    }

    public class LeaderboardResponse
    {
        public bool Success { get; set; }
        public required List<LeaderboardPlayer> Leaderboard { get; set; }
        public int Total { get; set; }
        public required string[] Sort_by { get; set; }
        public int Limit { get; set; }
    }

    public class LeaderboardPlayer
    {
        public int Rank { get; set; }
        public int Player_id { get; set; }
        public required string Player_name { get; set; }
        public required Dictionary<string, object> Player_data { get; set; }
    }

    // Exception and Error Handling Classes
    public class ApiException : Exception
    {
        public string? ErrorResponse { get; }
        public string? ApiError { get; }

        public ApiException(string message, string? errorResponse = null) : base(message)
        {
            ErrorResponse = errorResponse;
            ApiError = message;
        }

        public ApiException(string message, string? errorResponse, Exception innerException) : base(message, innerException)
        {
            ErrorResponse = errorResponse;
            ApiError = message;
        }
    }

    public class ErrorResponse : IApiResponse
    {
        public bool Success { get; set; }
        public string? Error { get; set; }
    }
}

Example Usage

Here's a complete example of how to use the michitai SDK in your C# application. This example demonstrates common operations like fetching game data, updating player information, and handling authentication.

using System;
using System.Threading.Tasks;
using System.Text.Json;
using System.Collections.Generic;
using michitai;

public class Game
{
    private static GameSDK? sdk;
    private static Dictionary<string, PlayerInfo> players = new Dictionary<string, PlayerInfo>();

    public static async Task Main()
    {
        Console.WriteLine("=== MICHITAI Game SDK Usage Example ===\n");

        // 1️⃣ Initialize SDK
        sdk = new GameSDK("YOUR_API_TOKEN", "YOUR_API_PRIVATE_TOKEN");
        Console.WriteLine("[INIT] SDK initialized\n");

        // 2️⃣ Register Multiple Players for Matchmaking Demo
        Console.WriteLine("[PLAYERS] Registering multiple players for matchmaking demo...");
        
        // Register Host Player
        var hostReg = await RegisterPlayer("GameHost", new { level = 10, rank = "gold", role = "host" });
        string hostToken = hostReg.Private_key;
        string hostId = hostReg.Player_id;
        Console.WriteLine($"[HOST] Registered: ID={hostId}, Token={hostToken.Substring(0, 8)}...\n");

        // Register Multiple Players
        var player1Reg = await RegisterPlayer("Player1", new { level = 8, rank = "silver", role = "player" });
        var player2Reg = await RegisterPlayer("Player2", new { level = 12, rank = "gold", role = "player" });
        var player3Reg = await RegisterPlayer("Player3", new { level = 6, rank = "bronze", role = "player" });
        
        players["host"] = new PlayerInfo { Id = hostId, Token = hostToken, Name = "GameHost" };
        players["player1"] = new PlayerInfo { Id = player1Reg.Player_id, Token = player1Reg.Private_key, Name = "Player1" };
        players["player2"] = new PlayerInfo { Id = player2Reg.Player_id, Token = player2Reg.Private_key, Name = "Player2" };
        players["player3"] = new PlayerInfo { Id = player3Reg.Player_id, Token = player3Reg.Private_key, Name = "Player3" };

        Console.WriteLine($"[PLAYERS] Total registered: {players.Count} players\n");

        // 3️⃣ Authenticate All Players
        Console.WriteLine("[AUTH] Authenticating all players...");
        foreach (var kvp in players)
        {
            var auth = await sdk.AuthenticatePlayer(kvp.Value.Token);
            if (auth.Success)
            {
                Console.WriteLine($"[AUTH] {kvp.Value.Name} authenticated successfully");
            }
        }
        Console.WriteLine();

        // 4️⃣ List all players (admin view)
        Console.WriteLine("[ADMIN] Fetching all players...");
        var allPlayers = await sdk.GetAllPlayers();
        Console.WriteLine($"[ADMIN] Total players in database: {allPlayers.Count}");
        foreach (var p in allPlayers.Players)
        {
            Console.WriteLine($" - ID={p.Id}, Name={p.Player_name}, Active={p.Is_active}");
        }
        Console.WriteLine();

        // 5️⃣ Get global game data
        Console.WriteLine("[GAME] Loading game data...");
        var gameData = await sdk.GetGameData();
        Console.WriteLine($"[GAME] Game ID={gameData.Game_id}, Settings={gameData.Data.Count}\n");

        // 6️⃣ Update global game data
        Console.WriteLine("[GAME] Updating game settings...");
        var updateGame = await sdk.UpdateGameData(new
        {
            game_settings = new { difficulty = "hard", max_players = 10, matchmaking_enabled = true },
            last_updated = DateTime.UtcNow.ToString("o")
        });
        Console.WriteLine($"[GAME] {updateGame.Message} at {updateGame.Updated_at}\n");

        // 7️⃣ Get player-specific data for host
        Console.WriteLine("[HOST] Loading host player data...");
        var hostData = await sdk.GetPlayerData(hostToken);
        Console.WriteLine($"[HOST] {hostData.Player_name} - Level: {GetJsonValue(hostData.Data, "level")}, Rank: {GetJsonValue(hostData.Data, "rank")}\n");

        // 8️⃣ Update host player data
        Console.WriteLine("[HOST] Updating host progress...");
        var updatedHost = await sdk.UpdatePlayerData(hostToken, new
        {
            level = 15,
            rank = "platinum",
            last_played = DateTime.UtcNow.ToString("o"),
            matchmaking_status = "ready"
        });
        Console.WriteLine($"[HOST] {updatedHost.Message} at {updatedHost.Updated_at}\n");

        // 9️⃣ Get server time
        Console.WriteLine("[SERVER] Getting server time...");
        var serverTime = await sdk.GetServerTime();
        if (serverTime.Success)
        {
            Console.WriteLine($"[SERVER] Server time (UTC): {serverTime.Utc}");
            Console.WriteLine($"[SERVER] Timestamp: {serverTime.Timestamp}");
            Console.WriteLine($"[SERVER] Readable: {serverTime.Readable}\n");
        }
        else
        {
            Console.WriteLine("[SERVER] Failed to get server time\n");
        }

        // 9️⃣1️⃣ Get server time with +1 hour offset
        Console.WriteLine("[SERVER] Getting server time with +1 hour offset...");
        var serverTimePlus1 = await sdk.GetServerTimeWithOffset(1);
        if (serverTimePlus1.Success)
        {
            Console.WriteLine($"[SERVER] Server time (+1h): {serverTimePlus1.Utc}");
            Console.WriteLine($"[SERVER] Timestamp: {serverTimePlus1.Timestamp}");
            Console.WriteLine($"[SERVER] Readable: {serverTimePlus1.Readable}");
            Console.WriteLine($"[SERVER] Offset: {serverTimePlus1.Offset.Offset_hours}h ({serverTimePlus1.Offset.Offset_string})");
            Console.WriteLine($"[SERVER] Original UTC: {serverTimePlus1.Offset.Original_utc}\n");
        }
        else
        {
            Console.WriteLine("[SERVER] Failed to get server time with +1 offset\n");
        }

        // 9️⃣2️⃣ Get server time with -2 hours offset
        Console.WriteLine("[SERVER] Getting server time with -2 hours offset...");
        var serverTimeMinus2 = await sdk.GetServerTimeWithOffset(-2);
        if (serverTimeMinus2.Success)
        {
            Console.WriteLine($"[SERVER] Server time (-2h): {serverTimeMinus2.Utc}");
            Console.WriteLine($"[SERVER] Timestamp: {serverTimeMinus2.Timestamp}");
            Console.WriteLine($"[SERVER] Readable: {serverTimeMinus2.Readable}");
            Console.WriteLine($"[SERVER] Offset: {serverTimeMinus2.Offset.Offset_hours}h ({serverTimeMinus2.Offset.Offset_string})");
            Console.WriteLine($"[SERVER] Original UTC: {serverTimeMinus2.Offset.Original_utc}\n");
        }
        else
        {
            Console.WriteLine("[SERVER] Failed to get server time with -2 offset\n");
        }

        // 🔟 Create a traditional room (for comparison)
        Console.WriteLine("[ROOM] Creating a traditional game room...");
        var roomCreate = await sdk.CreateRoomAsync(
            hostToken,
            "Traditional Room",
            "test123",
            4
        );
        string roomId = roomCreate.Room_id;
        Console.WriteLine($"[ROOM] Created room: ID={roomId}, Name={roomCreate.Room_name}, Is Host={roomCreate.Is_host}\n");

        // 1️⃣1️⃣ List all available rooms
        Console.WriteLine("[ROOM] Fetching available rooms...");
        var rooms = await sdk.GetRoomsAsync();
        Console.WriteLine($"[ROOM] Found {rooms.Rooms.Count} room(s):");
        foreach (var room in rooms.Rooms)
        {
            Console.WriteLine($" - ID: {room.Room_id}, Name: {room.Room_name}, Players: {room.Current_players}/{room.Max_players}");
        }
        Console.WriteLine();

        // 1️⃣2️⃣ Leave the traditional room (cleanup)
        Console.WriteLine("[ROOM] Leaving the traditional game room...");
        try
        {
            // Send heartbeat before leaving room
            Console.WriteLine("[ROOM] Sending heartbeat before leaving room...");
            var heartbeat = await sdk.SendPlayerHeartbeatAsync(hostToken);
            if (heartbeat.Success)
            {
                Console.WriteLine($"[ROOM] Heartbeat sent: {heartbeat.Message}");
            }
            
            var leaveRoom = await sdk.LeaveRoomAsync(hostToken);
            Console.WriteLine($"[ROOM] {leaveRoom.Message}");
            
            // Logout after leaving room
            Console.WriteLine("[ROOM] Logging out after leaving room...");
            var logout = await sdk.LogoutPlayerAsync(hostToken);
            if (logout.Success)
            {
                Console.WriteLine($"[ROOM] {logout.Message}");
            }
        }
        catch (ApiException ex)
        {
            Console.WriteLine($"[ROOM] Leave error: {ex.ApiError}");
        }

        // ==================== MATCHMAKING DEMO ====================

        // 2️⃣4️⃣ List Matchmaking Lobbies
        Console.WriteLine("[MATCHMAKING] Listing available matchmaking lobbies...");
        var lobbies = await sdk.GetMatchmakingLobbiesAsync();
        Console.WriteLine($"[MATCHMAKING] Found {lobbies.Lobbies.Count} lobby(ies):");
        foreach (var lobby in lobbies.Lobbies)
        {
            Console.WriteLine($" - ID: {lobby.Matchmaking_id}, Host: {lobby.Host_name}, Players: {lobby.Current_players}/{lobby.Max_players}, Strict: {lobby.Strict_full == 1}");
        }
        Console.WriteLine();

        // 2️⃣5️⃣ Create Matchmaking Lobby
        Console.WriteLine("[MATCHMAKING] Host creating new matchmaking lobby...");
        var matchmakingCreate = await sdk.CreateMatchmakingLobbyAsync(
            hostToken,
            maxPlayers: 4,
            strictFull: true,
            joinByRequests: false,
            extraJsonString: new { minLevel = 5, rank = "silver", gameMode = "competitive" }
        );
        string matchmakingId = matchmakingCreate.Matchmaking_id;
        Console.WriteLine($"[MATCHMAKING] Lobby created: ID={matchmakingId}, Max Players={matchmakingCreate.Max_players}, Host={matchmakingCreate.Is_host}");
        Console.WriteLine($"[MATCHMAKING] Settings: Strict Full={matchmakingCreate.Strict_full}, Join by Requests={matchmakingCreate.Join_by_requests}\n");

        // 2️⃣6️⃣ Request to Join Matchmaking (Player 1)
        Console.WriteLine("[MATCHMAKING] Player1 requesting to join matchmaking...");
        var joinRequest = await sdk.RequestToJoinMatchmakingAsync(players["player1"].Token, matchmakingId);
        Console.WriteLine($"[MATCHMAKING] Join request sent: ID={joinRequest.Request_id}, Message={joinRequest.Message}\n");

        // 2️⃣7️⃣ Respond to Join Request (Host approves)
        Console.WriteLine("[MATCHMAKING] Host responding to join request...");
        try
        {
            var approveRequest = await sdk.RespondToJoinRequestAsync(hostToken, matchmakingId, GameSDK.MatchmakingRequestAction.Approve);
            Console.WriteLine($"[MATCHMAKING] Response: {approveRequest.Message}, Action={approveRequest.Action}, Request ID={approveRequest.Request_id}\n");
        }
        catch (ApiException ex)
        {
            Console.WriteLine($"[MATCHMAKING] Error responding to request: {ex.ApiError}");
            Console.WriteLine($"[MATCHMAKING] Server response: {ex.ErrorResponse}\n");
        }

        // 2️⃣8️⃣ Check Join Request Status (Player 1)
        Console.WriteLine("[MATCHMAKING] Player1 checking join request status...");
        var requestStatus = await sdk.CheckJoinRequestStatusAsync(players["player1"].Token, joinRequest.Request_id);
        if (requestStatus.Success)
        {
            var req = requestStatus.Request;
            Console.WriteLine($"[MATCHMAKING] Request Status: {req.Status}, Responded by: {req.Responder_name} at {req.Responded_at}");
        }
        Console.WriteLine();

        // 2️⃣9️⃣ Get Current Matchmaking Status (Host)
        Console.WriteLine("[MATCHMAKING] Host checking current matchmaking status...");
        var currentMatchmaking = await sdk.GetCurrentMatchmakingStatusAsync(hostToken);
        if (currentMatchmaking.Success && currentMatchmaking.In_matchmaking)
        {
            var mm = currentMatchmaking.Matchmaking;
            Console.WriteLine($"[MATCHMAKING] Host Status: In Lobby={currentMatchmaking.In_matchmaking}, Is Host={mm.Is_host}");
            Console.WriteLine($"[MATCHMAKING] Lobby: {mm.Matchmaking_id}, Players: {mm.Current_players}/{mm.Max_players}");
            Console.WriteLine($"[MATCHMAKING] Settings: Strict={mm.Strict_full}, Approval={mm.Join_by_requests}, Started={mm.Is_started}");
        }
        Console.WriteLine();

        // 3️⃣0️⃣ Join Matchmaking Directly (Player 2 - lobby allows direct join)
        Console.WriteLine("[MATCHMAKING] Player2 joining directly (no approval required for this demo)...");
        var directJoin = await sdk.JoinMatchmakingDirectlyAsync(players["player2"].Token, matchmakingId);
        Console.WriteLine($"[MATCHMAKING] {directJoin.Message}, Lobby ID={directJoin.Matchmaking_id}\n");

        // 3️⃣1️⃣ Leave Matchmaking (Player 1 leaves to rejoin)
        Console.WriteLine("[MATCHMAKING] Player1 leaving matchmaking to test rejoin...");
        try
        {
            var leaveMatchmaking = await sdk.LeaveMatchmakingAsync(players["player1"].Token);
            Console.WriteLine($"[MATCHMAKING] {leaveMatchmaking.Message}\n");
        }
        catch (ApiException ex)
        {
            Console.WriteLine($"[MATCHMAKING] Leave error: {ex.ApiError}");
            Console.WriteLine($"[MATCHMAKING] Note: Player1 was not in matchmaking lobby\n");
        }

        // 3️⃣2️⃣ List Matchmaking Players (Host view)
        Console.WriteLine("[MATCHMAKING] Host listing players in matchmaking lobby...");
        var matchmakingPlayers = await sdk.GetMatchmakingPlayersAsync(hostToken);
        Console.WriteLine($"[MATCHMAKING] Players in lobby ({matchmakingPlayers.Players.Count}):");
        foreach (var player in matchmakingPlayers.Players)
        {
            Console.WriteLine($" - {player.Player_name} (ID: {player.Player_id}, Host: {player.Is_host == 1}, Status: {player.Status}, Heartbeat: {player.Seconds_since_heartbeat}s ago)");
        }
        Console.WriteLine();

        // 3️⃣3️⃣ Send Matchmaking Heartbeat (All players)
        Console.WriteLine("[MATCHMAKING] All players sending heartbeats...");
        foreach (var kvp in players)
        {
            if (kvp.Key != "player1") // Player1 left
            {
                try
                {
                    var heartbeat = await sdk.SendMatchmakingHeartbeatAsync(kvp.Value.Token);
                    Console.WriteLine($"[MATCHMAKING] {kvp.Value.Name} heartbeat: {heartbeat.Status ?? "N/A"}");
                }
                catch (ApiException ex)
                {
                    Console.WriteLine($"[MATCHMAKING] {kvp.Value.Name} heartbeat error: {ex.ApiError}");
                }
            }
        }
        Console.WriteLine();

        // Player1 rejoins
        Console.WriteLine("[MATCHMAKING] Player1 rejoining matchmaking...");
        var rejoin = await sdk.JoinMatchmakingDirectlyAsync(players["player1"].Token, matchmakingId);
        Console.WriteLine($"[MATCHMAKING] {rejoin.Message}\n");

        // 3️⃣4️⃣ Remove Matchmaking Lobby (Host removes - cleanup)
        Console.WriteLine("[MATCHMAKING] Host removing matchmaking lobby (cleanup test)...");
        var removeLobby = await sdk.RemoveMatchmakingLobbyAsync(hostToken);
        Console.WriteLine($"[MATCHMAKING] {removeLobby.Message}\n");

        // Create new lobby for game start demo
        Console.WriteLine("[MATCHMAKING] Creating new lobby for game start demo...");
        var newLobby = await sdk.CreateMatchmakingLobbyAsync(
            hostToken,
            maxPlayers: 3,
            strictFull: false,
            joinByRequests: false
        );
        string newMatchmakingId = newLobby.Matchmaking_id;
        Console.WriteLine($"[MATCHMAKING] New lobby created: {newMatchmakingId}\n");

        // All players join the new lobby
        Console.WriteLine("[MATCHMAKING] All players joining new lobby...");
        foreach (var kvp in players)
        {
            if (kvp.Key != "host") // Host is already in
            {
                try
                {
                    var join = await sdk.JoinMatchmakingDirectlyAsync(kvp.Value.Token, newMatchmakingId);
                    Console.WriteLine($"[MATCHMAKING] {kvp.Value.Name}: {join.Message}");
                }
                catch (ApiException ex)
                {
                    Console.WriteLine($"[MATCHMAKING] {kvp.Value.Name}: Join error - {ex.ApiError}");
                }
            }
        }
        Console.WriteLine();

        // 3️⃣5️⃣ Start Game from Matchmaking (Host starts the game)
        Console.WriteLine("[MATCHMAKING] Host starting game from matchmaking lobby...");
        var startGame = await sdk.StartGameFromMatchmakingAsync(hostToken);
        Console.WriteLine($"[MATCHMAKING] {startGame.Message}");
        Console.WriteLine($"[MATCHMAKING] Game Room Created: ID={startGame.Room_id}, Name={startGame.Room_name}");
        Console.WriteLine($"[MATCHMAKING] Players Transferred: {startGame.Players_transferred}\n");

        // ==================== POST-MATCHMAKING GAME ROOM DEMO ====================

        // Now the host is also a room host - demonstrate room functionality
        Console.WriteLine("[POST-MATCHMAKING] Demonstrating room functionality with matchmaking host...");

        // 1️⃣2️⃣ List all available rooms (should include the new game room)
        Console.WriteLine("[ROOM] Fetching available rooms after matchmaking...");
        var postRooms = await sdk.GetRoomsAsync();
        Console.WriteLine($"[ROOM] Found {postRooms.Rooms.Count} room(s):");
        foreach (var room in postRooms.Rooms)
        {
            Console.WriteLine($" - ID: {room.Room_id}, Name: {room.Room_name}, Players: {room.Current_players}/{room.Max_players}");
        }
        Console.WriteLine();

        // 1️⃣3️⃣ List players in the new game room
        Console.WriteLine("[ROOM] Fetching players in the new game room...");
        var newRoomPlayers = await sdk.GetRoomPlayersAsync(hostToken);
        Console.WriteLine($"[ROOM] Players in new room ({newRoomPlayers.Players.Count}):");
        foreach (var player in newRoomPlayers.Players)
        {
            Console.WriteLine($" - {player.Player_name} (ID: {player.Player_id}, Host: {player.Is_host == 1}, Online: {player.Is_online})");
        }
        Console.WriteLine();

        // 1️⃣5️⃣ Submit actions from different players
        Console.WriteLine("[ACTION] Players submitting actions in the new game room...");
        foreach (var kvp in players)
        {
            try
            {
                var action = await sdk.SubmitActionAsync(
                        kvp.Value.Token,
                        "player_ready",
                        new { player_id = kvp.Value.Id, ready = true, timestamp = DateTime.UtcNow.ToString("o") }
                    );
                Console.WriteLine($"[ACTION] {kvp.Value.Name} submitted ready action: {action.Action_id}");
            }
            catch (ApiException ex)
            {
                Console.WriteLine($"[ACTION] {kvp.Value.Name}: Action submit error - {ex.ApiError}");
            }
        }
        Console.WriteLine();

        // 1️⃣6️⃣ Check pending actions
        Console.WriteLine("[ACTION] Checking for pending actions...");
        var pendingActions = await sdk.GetPendingActionsAsync(hostToken);
        if (pendingActions.Actions != null && pendingActions.Actions.Count > 0)
        {
            Console.WriteLine($"[ACTION] Found {pendingActions.Actions.Count} pending actions:");
            foreach (var pendingAction in pendingActions.Actions)
            {
                Console.WriteLine($"[ACTION] - {pendingAction.Player_name}: {pendingAction.Action_type} at {pendingAction.Created_at}");
            }
        }
        Console.WriteLine();

        // 2️⃣0️⃣ Send game start update to all players
        Console.WriteLine("[UPDATE] Host sending game start update to all players...");
        var gameStartUpdate = await sdk.UpdatePlayersAsync(
            hostToken,
            new UpdatePlayersRequest(
                "all",
                "game_start",
                new { game_mode = "competitive", start_time = DateTime.UtcNow.ToString("o") }
            )
        );
        Console.WriteLine($"[UPDATE] Game start update sent to {gameStartUpdate.Updates_sent} players\n");

        // 2️⃣2️⃣ Poll for game updates
        Console.WriteLine("[UPDATE] Players polling for game updates...");
        foreach (var kvp in players)
        {
            try
            {
                var pollUpdates = await sdk.PollUpdatesAsync(kvp.Value.Token);
                if (pollUpdates.Updates.Count > 0)
                {
                    Console.WriteLine($"[UPDATE] {kvp.Value.Name} received {pollUpdates.Updates.Count} update(s):");
                    foreach (var update in pollUpdates.Updates)
                    {
                        Console.WriteLine($"[UPDATE]   - {update.Type} from {update.From_player_id} at {update.Created_at}");
                    }
                }
                else
                {
                    Console.WriteLine($"[UPDATE] {kvp.Value.Name}: No new updates");
                }
            }
            catch (ApiException ex)
            {
                Console.WriteLine($"[UPDATE] {kvp.Value.Name}: Poll error - {ex.ApiError}");
            }
        }
        Console.WriteLine();

        // 2️⃣3️⃣ Get current room information for all players
        Console.WriteLine("[ROOM] All players checking current room status...");
        foreach (var kvp in players)
        {
            try
            {
                var currentRoom = await sdk.GetCurrentRoomAsync(kvp.Value.Token);
                if (currentRoom.Success && currentRoom.In_room && currentRoom.Room != null)
                {
                    var room = currentRoom.Room;
                    Console.WriteLine($"[ROOM] {kvp.Value.Name}: In '{room.Room_name}' (Players: {room.Current_players}/{room.Max_players}, Host: {room.Is_host})");
                }
                else
                {
                    Console.WriteLine($"[ROOM] {kvp.Value.Name}: Not in any room");
                }
            }
            catch (ApiException ex)
            {
                Console.WriteLine($"[ROOM] {kvp.Value.Name}: Room status error - {ex.ApiError}");
            }
        }
        Console.WriteLine();

        // 2️⃣4️⃣ Leave the room (all players)
        Console.WriteLine("[ROOM] All players leaving the game room...");
        foreach (var kvp in players)
        {
            try
            {
                // Send heartbeat before leaving room
                Console.WriteLine($"[ROOM] {kvp.Value.Name}: Sending heartbeat before leaving room...");
                var heartbeat = await sdk.SendPlayerHeartbeatAsync(kvp.Value.Token);
                if (heartbeat.Success)
                {
                    Console.WriteLine($"[ROOM] {kvp.Value.Name}: Heartbeat sent: {heartbeat.Message}");
                }
                
                var leaveRoom = await sdk.LeaveRoomAsync(kvp.Value.Token);
                Console.WriteLine($"[ROOM] {kvp.Value.Name}: {leaveRoom.Message}");
                
                // Logout after leaving room
                Console.WriteLine($"[ROOM] {kvp.Value.Name}: Logging out after leaving room...");
                var logout = await sdk.LogoutPlayerAsync(kvp.Value.Token);
                if (logout.Success)
                {
                    Console.WriteLine($"[ROOM] {kvp.Value.Name}: {logout.Message}");
                }
            }
            catch (ApiException ex)
            {
                Console.WriteLine($"[ROOM] {kvp.Value.Name}: Leave error - {ex.ApiError}");
            }
        }
        Console.WriteLine();

        // 2️⃣5️⃣ Leaderboards
        Console.WriteLine("[LEADERBOARD] Testing leaderboard functionality...");
        
        try
        {
            // Sort by level only
            Console.WriteLine("[LEADERBOARD] Getting leaderboard sorted by level...");
            var leaderboardLevel = await sdk.GetLeaderboardAsync(new[] { "level" }, 10);
            if (leaderboardLevel.Success)
            {
                Console.WriteLine($"[LEADERBOARD] Success! Total players: {leaderboardLevel.Total}");
                Console.WriteLine($"[LEADERBOARD] Sorted by: {string.Join(", ", leaderboardLevel.Sort_by)}");
                Console.WriteLine("[LEADERBOARD] Top players:");
                foreach (var player in leaderboardLevel.Leaderboard.Take(5))
                {
                    Console.WriteLine($"[LEADERBOARD]   Rank {player.Rank}: {player.Player_name} (ID: {player.Player_id})");
                    if (player.Player_data != null)
                    {
                        Console.WriteLine($"[LEADERBOARD]     Level: {player.Player_data.GetValueOrDefault("level", "N/A")}");
                        Console.WriteLine($"[LEADERBOARD]     Rank: {player.Player_data.GetValueOrDefault("rank", "N/A")}");
                    }
                }
            }
            else
            {
                Console.WriteLine("[LEADERBOARD] Failed to get leaderboard by level");
            }
            Console.WriteLine();

            // Sort by level then score
            Console.WriteLine("[LEADERBOARD] Getting leaderboard sorted by level, then score...");
            var leaderboardLevelScore = await sdk.GetLeaderboardAsync(new[] { "level", "score" }, 10);
            if (leaderboardLevelScore.Success)
            {
                Console.WriteLine($"[LEADERBOARD] Success! Total players: {leaderboardLevelScore.Total}");
                Console.WriteLine($"[LEADERBOARD] Sorted by: {string.Join(", ", leaderboardLevelScore.Sort_by)}");
                Console.WriteLine("[LEADERBOARD] Top players:");
                foreach (var player in leaderboardLevelScore.Leaderboard.Take(5))
                {
                    Console.WriteLine($"[LEADERBOARD]   Rank {player.Rank}: {player.Player_name} (ID: {player.Player_id})");
                    if (player.Player_data != null)
                    {
                        Console.WriteLine($"[LEADERBOARD]     Level: {player.Player_data.GetValueOrDefault("level", "N/A")}");
                        Console.WriteLine($"[LEADERBOARD]     Score: {player.Player_data.GetValueOrDefault("score", "N/A")}");
                        var inventory = player.Player_data.GetValueOrDefault("inventory", null);
                        if (inventory != null && inventory.ToString() != "")
                        {
                            Console.WriteLine($"[LEADERBOARD]     Inventory: {inventory}");
                        }
                    }
                }
            }
            else
            {
                Console.WriteLine("[LEADERBOARD] Failed to get leaderboard by level and score");
            }
            Console.WriteLine();

            // Sort by score then level
            Console.WriteLine("[LEADERBOARD] Getting leaderboard sorted by score, then level...");
            var leaderboardScoreLevel = await sdk.GetLeaderboardAsync(new[] { "score", "level" }, 10);
            if (leaderboardScoreLevel.Success)
            {
                Console.WriteLine($"[LEADERBOARD] Success! Total players: {leaderboardScoreLevel.Total}");
                Console.WriteLine($"[LEADERBOARD] Sorted by: {string.Join(", ", leaderboardScoreLevel.Sort_by)}");
                Console.WriteLine("[LEADERBOARD] Top players:");
                foreach (var player in leaderboardScoreLevel.Leaderboard.Take(5))
                {
                    Console.WriteLine($"[LEADERBOARD]   Rank {player.Rank}: {player.Player_name} (ID: {player.Player_id})");
                    if (player.Player_data != null)
                    {
                        Console.WriteLine($"[LEADERBOARD]     Score: {player.Player_data.GetValueOrDefault("score", "N/A")}");
                        Console.WriteLine($"[LEADERBOARD]     Level: {player.Player_data.GetValueOrDefault("level", "N/A")}");
                        var inventory = player.Player_data.GetValueOrDefault("inventory", null);
                        if (inventory != null && inventory.ToString() != "")
                        {
                            Console.WriteLine($"[LEADERBOARD]     Inventory: {inventory}");
                        }
                    }
                }
            }
            else
            {
                Console.WriteLine("[LEADERBOARD] Failed to get leaderboard by score and level");
            }
        }
        catch (ApiException ex)
        {
            Console.WriteLine($"[LEADERBOARD] Error: {ex.ApiError}");
        }
        Console.WriteLine();

        Console.WriteLine("=== Complete Matchmaking & Game Demo ===");
        Console.WriteLine("✅ Multiple players registered and authenticated");
        Console.WriteLine("✅ Matchmaking lobby created with custom settings");
        Console.WriteLine("✅ Join requests and approval system demonstrated");
        Console.WriteLine("✅ Direct join functionality tested");
        Console.WriteLine("✅ Player management and heartbeats working");
        Console.WriteLine("✅ Game started from matchmaking lobby");
        Console.WriteLine("✅ Host transitioned from matchmaking to room host");
        Console.WriteLine("✅ Full game room functionality demonstrated");
        Console.WriteLine("✅ Leaderboard system with multiple sorting options tested");
    }

    private static async Task<PlayerRegisterResponse> RegisterPlayer(string name, object playerData)
    {
        Console.WriteLine($"[PLAYER] Registering {name}...");
        var reg = await sdk.RegisterPlayer(name, playerData);
        Console.WriteLine($"[PLAYER] {name} registered: ID={reg.Player_id}");
        return reg;
    }

    private static string GetJsonValue(Dictionary<string, object> data, string key)
    {
        if (data.ContainsKey(key))
        {
            return data[key].ToString();
        }
        return "N/A";
    }

    private class PlayerInfo
    {
        public required string Id { get; set; }
        public required string Token { get; set; }
        public required string Name { get; set; }
    }



    public class ConsoleLogger : ILogger
    {
        public virtual void Error(string message)
        {
            Console.WriteLine($"[Error] {message}");
        }

        public virtual void Log(string message)
        {
            Console.WriteLine($"[Log] {message}");
        }

        public virtual void Warn(string message)
        {
            Console.WriteLine($"[Warning] {message}");
        }
    }

#if UNITY_EDITOR
    public class UnityLogger : ILogger
    {
        public virtual void Error(string message)
        {
            Debug.Log($"[Error] {message}");
        }

        public virtual void Log(string message)
        {
            Debug.LogWarning($"[Log] {message}");
        }

        public virtual void Warn(string message)
        {
            Debug.LogError($"[Warning] {message}");
        }
    }
#endif
}
How to use this example
  1. Create a new C# console application in Visual Studio or your preferred IDE
  2. Add the downloaded SDK.cs file to your project
  3. Copy this example code into your Program.cs file
  4. Replace your-api-key-here with your actual API key
  5. Run the application to see the SDK in action

Multiplayer API – Core Cells

Use our REST API directly from any platform or language. All endpoints return JSON responses with consistent error handling.

POST /v1/php/game_players.php?api_token=YOUR_API_KEY

Description: Creates a new player in the game. Returns a player_id and private_key needed for future requests.

Request Body:
{
  "player_name": "TestPlayer",
  "player_data": {
    "level": 1,
    "score": 0,
    "inventory": ["sword","shield"]
  }
}
Response:
{
  "success": true,
  "player_id": "7",
  "private_key": "46702c9b906e3361c26dbcd605ee9183",
  "player_name": "TestPlayer",
  "game_id": 4
}
PUT /v1/php/game_players.php?api_token=YOUR_API_KEY&game_player_token=PLAYER_PRIVATE_KEY

Description: Updates player info such as active status. This does not change player data like level or inventory (those are in /game_data.php).

Request Body:
{}
Response:
{
  "success": true,
  "player": {
    "id": 7,
    "game_id": 4,
    "player_name": "TestPlayer",
    "player_data": {
      "level": 1,
      "score": 0,
      "inventory": ["sword","shield"]
    },
    "is_active": 1,
    "last_login": null,
    "created_at": "2026-01-13 14:21:16",
    "updated_at": "2026-01-13 14:21:16"
  }
}
POST /v1/php/game_players.php/heartbeat?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Update player heartbeat to track activity and maintain online status.

Request Body:
{}
Response:
{
  "success": true,
  "message": "Heartbeat updated",
  "last_heartbeat": "2026-03-13 09:46:13"
}
POST /v1/php/game_players.php/logout?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Logout player and update last_logout timestamp. Sets is_active to 0.

Request Body:
{}
Response:
{
  "success": true,
  "message": "Player logged out successfully",
  "last_logout": "2026-03-13 09:46:37"
}
GET /v1/php/game_players.php?api_token=YOUR_API_KEY&api_private_token=YOUR_API_PRIVATE_TOKEN

Description: Retrieves a list of all players in the game. Useful for admin dashboards or multiplayer matchmaking.

Request Body:
{}
Response:
{
  "success": true,
  "count": 7,
  "players": [
    {"id":3,"player_name":"TestPlayer","is_active":1,"last_login":null,"created_at":"2026-01-13 12:30:47"},
    {"id":7,"player_name":"TestPlayer","is_active":1,"last_login":"2026-01-13 14:22:33","created_at":"2026-01-13 14:21:16"}
}
GET /v1/php/game_data.php/game/get?api_token=YOUR_API_KEY

Description: Retrieves the global game data, including text, settings, and last update timestamp. Used to sync clients with the server.

Request Body:
{}
Response:
{
  "success": true,
  "type": "game",
  "game_id": 4,
  "data": {
    "text": "hello world",
    "game_settings": {
      "difficulty": "hard",
      "max_players": 10
    },
    "last_updated": "2025-01-13T12:00:00Z"
  }
}
PUT /v1/php/game_data.php/game/update?api_token=YOUR_API_KEY&api_private_token=YOUR_API_PRIVATE_TOKEN

Description: Updates global game data. For example, changing settings or max players. Requires API key authentication.

Request Body:
{
  "game_settings": {
    "difficulty": "hard",
    "max_players": 10
  },
  "last_updated": "2025-01-13T12:00:00Z"
}
Response:
{
  "success": true,
  "message": "Game data updated successfully",
  "updated_at": "2026-01-13 14:24:23"
}
GET /v1/php/game_data.php/player/get?api_token=YOUR_API_KEY&game_player_token=PLAYER_PRIVATE_KEY

Description: Retrieves a specific player's data using their private_key. Includes level, score, and inventory.

Request Body:
{}
Response:
{
  "success": true,
  "type": "player",
  "player_id": 7,
  "player_name": "TestPlayer",
  "data": {
    "level": 1,
    "score": 0,
    "inventory": ["sword","shield"]
  }
}
PUT /v1/php/game_data.php/player/update?api_token=YOUR_API_KEY&game_player_token=PLAYER_PRIVATE_KEY

Description: Updates a specific player's data like level, score, inventory, and last played timestamp.

Request Body:
{
  "level": 2,
  "score": 100,
  "inventory": ["sword","shield","potion"],
  "last_played": "2025-01-13T12:30:00Z"
}
Response:
{
  "success": true,
  "message": "Player data updated successfully",
  "updated_at": "2026-01-13 14:27:10"
}
GET /v1/php/time.php?api_token=YOUR_API_KEY

Description: Retrieves current server time in multiple formats including UTC timestamp and human-readable format.

Request Body:
{}
Response:
{
  "success": true,
  "utc": "2026-03-13T14:01:49+00:00",
  "timestamp": 1773410509,
  "readable": "2026-03-13 14:01:49 UTC"
}
GET /v1/php/time.php?api_token=YOUR_API_KEY&utc=+1

Description: Retrieves server time with +1 hour offset adjustment.

Request Body:
{}
Response:
{
  "success": true,
  "utc": "2026-03-13T15:04:17+00:00",
  "timestamp": 1773414257,
  "readable": "2026-03-13 15:04:17 UTC",
  "offset": {
    "offset_hours": 1,
    "offset_string": "+1",
    "original_utc": "2026-03-13T14:04:17+00:00",
    "original_timestamp": 1773410657
  }
}
GET /v1/php/time.php?api_token=YOUR_API_KEY&utc=-2

Description: Retrieves server time with -2 hours offset adjustment.

Request Body:
{}
Response:
{
  "success": true,
  "utc": "2026-03-13T12:05:13+00:00",
  "timestamp": 1773403513,
  "readable": "2026-03-13 12:05:13 UTC",
  "offset": {
    "offset_hours": -2,
    "offset_string": "-2",
    "original_utc": "2026-03-13T14:05:13+00:00",
    "original_timestamp": 1773410713
  }
}
GET /v1/php/matchmaking.php/list?api_token=YOUR_API_TOKEN

Description: Lists all available matchmaking lobbies that are not full and not started.

Request Body:
{}
Response:
{
  "success": true,
  "lobbies": [
    {
      "matchmaking_id": "15b2b6e5f0ba44b5eef77705d120861f",
      "host_player_id": 62,
      "max_players": 4,
      "strict_full": 1,
      "extra_json_string": {"minLevel":10,"rank":"gold"},
      "created_at": "2026-03-10 15:16:58",
      "last_heartbeat": "2026-03-10 15:16:58",
      "current_players": 1,
      "host_name": "TestPlayer"
    }
  ]
}
POST /v1/php/matchmaking.php/create?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Creates a new matchmaking lobby. The creating player automatically becomes the host.

Request Body:
{
  "maxPlayers": 4,
  "strictFull": true,
  "joinByRequests": true,
  "extraJsonString": {"minLevel":10,"rank":"gold"}
}
Response:
{
  "success": true,
  "matchmaking_id": "636b3ffc9b30dc9c918d8a49661df078",
  "max_players": 4,
  "strict_full": true,
  "join_by_requests": true,
  "is_host": true
}
POST /v1/php/matchmaking.php/{matchmaking_id}/request?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Requests to join a matchmaking lobby that requires host approval.

Request Body:
{}
Response:
{
  "success": true,
  "request_id": "82334acd88f0af6a1f4747bbe755263a",
  "message": "Join request sent to host"
}
POST /v1/php/matchmaking.php/{matchmaking_id}/response?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Host responds to a join request (approve or reject).

Request Body:
{
  "action": "approve"
}
Response:
{
  "success": true,
  "message": "Join request approved successfully",
  "request_id": "f4d90025b5de54e6b1a83940cffb4490",
  "action": "approve"
}
POST /v1/php/matchmaking.php/{matchmaking_id}/response?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Host rejects a join request.

Request Body:
{
  "action": "reject"
}
Response:
{
  "success": true,
  "message": "Join request rejected successfully",
  "request_id": "f4d90025b5de54e6b1a83940cffb4490",
  "action": "reject"
}
GET /v1/php/matchmaking.php/{matchmaking_id}/status?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Checks the status of a join request.

Request Body:
{}
Response:
{
  "success": true,
  "request": {
    "request_id": "f4d90025b5de54e6b1a83940cffb4490",
    "matchmaking_id": "15b2b6e5f0ba44b5eef77705d120861f",
    "status": "approved",
    "requested_at": "2026-03-10 16:10:23",
    "responded_at": "2026-03-10 16:18:43",
    "responded_by": 62,
    "responder_name": "TestPlayer",
    "join_by_requests": true
  }
}
GET /v1/php/matchmaking.php/current?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Gets the current player's matchmaking status and lobby information.

Request Body:
{}
Response:
{
  "success": true,
  "in_matchmaking": true,
  "matchmaking": {
    "matchmaking_id": "636b3ffc9b30dc9c918d8a49661df078",
    "is_host": true,
    "max_players": 4,
    "current_players": 1,
    "strict_full": true,
    "join_by_requests": false,
    "extra_json_string": {"minLevel":10,"rank":"gold"},
    "joined_at": "2026-03-06 17:23:53",
    "player_status": "active",
    "last_heartbeat": "2026-03-06 17:23:53",
    "lobby_heartbeat": "2026-03-06 17:24:37",
    "is_started": false,
    "started_at": null
  },
  "pending_requests": []
}
POST /v1/php/matchmaking.php/{matchmaking_id}/join?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Joins a matchmaking lobby directly (only works if lobby doesn't require approval).

Request Body:
{}
Response:
{
  "success": true,
  "matchmaking_id": "15b2b6e5f0ba44b5eef77705d120861f",
  "message": "Successfully joined matchmaking lobby"
}
POST /v1/php/matchmaking.php/leave?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Leaves the current matchmaking lobby.

Request Body:
{}
Response:
{
  "success": true,
  "message": "Successfully left matchmaking lobby"
}
GET /v1/php/matchmaking.php/players?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Lists all players in the current matchmaking lobby.

Request Body:
{}
Response:
{
  "success": true,
  "players": [
    {
      "player_id": 47,
      "joined_at": "2026-03-06 17:23:53",
      "last_heartbeat": "2026-03-06 17:23:53",
      "status": "active",
      "player_name": "TestPlayer",
      "seconds_since_heartbeat": 726,
      "is_host": 1
    },
    {
      "player_id": 46,
      "joined_at": "2026-03-06 17:35:01",
      "last_heartbeat": "2026-03-06 17:35:01",
      "status": "active",
      "player_name": "TestPlayer",
      "seconds_since_heartbeat": 58,
      "is_host": 0
    }
  ],
  "last_updated": "2026-03-06T17:35:59+00:00"
}
POST /v1/php/matchmaking.php/heartbeat?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Sends a heartbeat to keep the player active in the matchmaking lobby.

Request Body:
{}
Response:
{
  "success": true,
  "status": "ok"
}
POST /v1/php/matchmaking.php/remove?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Removes the matchmaking lobby (host only).

Request Body:
{}
Response:
{
  "success": true,
  "message": "Matchmaking lobby removed successfully"
}
POST /v1/php/matchmaking.php/start?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Starts a game from the matchmaking lobby (host only).

Request Body:
{}
Response:
{
  "success": true,
  "room_id": "c899e32506d44823d486585b247eafe5",
  "room_name": "Game from Matchmaking 15b2b6",
  "players_transferred": 2,
  "message": "Game started successfully"
}
POST /v1/php/game_room.php/rooms?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Creates a new game room. The creating player automatically becomes the host.

Request Body:
{
  "room_name": "My Game Room",
  "password": "secret123",
  "max_players": 4
}
Response:
{
  "success": true,
  "room_id": "dc3723848639139113ca240958ba0bf8",
  "room_name": "My Game Room",
  "is_host": true
}
GET /v1/php/game_room.php/rooms?api_token=YOUR_API_TOKEN

Description: Returns list of currently available game rooms (not full, public/visible).

Request Body:
{}
Response:
{
  "success": true,
  "rooms": [
    {
      "room_id": "dc3723848639139113ca240958ba0bf8",
      "room_name": "My Game Room",
      "max_players": 4,
      "current_players": 1,
      "has_password": true
    }
  ]
}
POST /v1/php/game_room.php/rooms/{room_id}/join?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Join an existing room by ID. Password is required if the room is protected.

Request Body:
{
  "password": "secret123"
}
Response:
{
  "success": true,
  "room_id": "dc3723848639139113ca240958ba0bf8",
  "message": "Successfully joined the room"
}
GET /v1/php/game_room.php/players?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Returns list of players currently in the same room as you.

Request Body:
{}
Response:
{
  "success": true,
  "players": [
    {
      "player_id": "1",
      "player_name": "TestPlayer",
      "is_host": false,
      "is_online": true
    }
  ],
  "last_updated": "2026-01-15T16:06:42Z"
}
POST /v1/php/game_room.php/rooms/leave?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Leave the current room. If you were the host, a new host may be assigned automatically.

Request Body:
{}
Response:
{
  "success": true,
  "message": "Successfully left the room"
}
POST /v1/php/game_room.php/heartbeat?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Updates player's last activity timestamp (used to determine online status).

Request Body:
{}
Response:
{
  "success": true,
  "status": "ok"
}
POST /v1/php/game_room.php/actions?api_token=YOUR_API_TOKEN&game_player_token=PLAYER_PRIVATE_KEY

Description: Submits a new player action to the room's action queue (movement, attack, item use, etc.). The action is initially marked as pending and awaits server/host processing.

Request Body:
{
  "action_type": "move",
  "request_data": {
    "x": 10,
    "y": 20
  }
}
Response:
{
  "success": true,
  "action_id": "1c2bbd859e36dc7d7e5e9b4f263c88ce",
  "status": "pending"
}
GET /v1/php/game_room.php/actions/poll?api_token=YOUR_API_TOKEN&game_player_token=PLAYER_PRIVATE_KEY

Description: Retrieves recently completed/processed actions from the room. Clients should poll this endpoint regularly to receive updates on action results.

Request Body:
{}
Response:
{
  "success": true,
  "actions": [
    {
      "action_id": "1c2bbd859e36dc7d7e5e9b4f263c88ce",
      "action_type": "move",
      "response_data": "{\"success\":true,\"message\":\"Moved successfully\"}",
      "status": "completed"
    }
  ]
}
GET /v1/php/game_room.php/actions/pending?api_token=YOUR_API_TOKEN&game_player_token=PLAYER_PRIVATE_KEY

Description: Returns list of currently pending actions in the room. Typically used by the host or server-side logic to process/approve actions.

Request Body:
{}
Response:
{
  "success": true,
  "actions": [
    {
      "action_id": "1c2bbd859e36dc7d7e5e9b4f263c88ce",
      "player_id": "1",
      "action_type": "move",
      "request_data": "{\"x\":10,\"y\":20}",
      "created_at": "2026-01-15 16:10:43",
      "player_name": "TestPlayer"
    }
  ]
}
POST /v1/php/game_room.php/actions/{action_id}/complete?api_token=YOUR_API_TOKEN&game_player_token=PLAYER_PRIVATE_KEY

Description: Marks a pending action as completed and attaches the result/response data. Usually called by the room host or authoritative server.

Request Body:
{
  "status": "completed",
  "response_data": {
    "success": true,
    "message": "Moved successfully"
  }
}
Response:
{
  "success": true,
  "message": "Action completed"
}
POST /v1/php/game_room.php/updates?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Send updates to all players in the room (host only).

Request Body:
{
  "targetPlayerIds": "all",
  "type": "play_animation",
  "dataJson": {
    "animation": "victory",
    "duration": 2.0
  }
}
Response:
{
  "success": true,
  "updates_sent": 1,
  "update_ids": ["ddb19c9d8722073762f5db33ff13712a"],
  "target_players": ["47"]
}
POST /v1/php/game_room.php/updates?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Send updates to specific players in the room (host only).

Request Body:
{
  "targetPlayerIds": ["47"],
  "type": "spawn_effect",
  "dataJson": {
    "effect": "explosion",
    "position": {
      "x": 10,
      "y": 20
    }
  }
}
Response:
{
  "success": true,
  "updates_sent": 1,
  "update_ids": ["377bfa1d4c56c3f72d9c87b0c081e6e8"],
  "target_players": ["47"]
}
GET /v1/php/game_room.php/updates/poll?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Poll for updates sent to the current player.

Request Body:
{}
Response:
{
  "success": true,
  "updates": [
    {
      "update_id": "a28388775fcf9478c6926cbe44f9d3ed",
      "from_player_id": "48",
      "type": "play_animation",
      "data_json": {
        "animation": "victory",
        "duration": 2
      },
      "created_at": "2026-03-09 10:51:40"
    },
    {
      "update_id": "f26cbcdab3939b968f148edf68a9fe54",
      "from_player_id": "48",
      "type": "play_animation",
      "data_json": {
        "animation": "victory",
        "duration": 2
      },
      "created_at": "2026-03-09 10:53:58"
    }
  ],
  "last_update_id": "f26cbcdab3939b968f148edf68a9fe54"
}
GET /v1/php/game_room.php/updates/poll?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN&lastUpdateId=UPDATE_ID

Description: Poll for updates after a specific update ID (incremental polling).

Request Body:
{}
Response:
{
  "success": true,
  "updates": [
    {
      "update_id": "f26cbcdab3939b968f148edf68a9fe54",
      "from_player_id": "48",
      "type": "play_animation",
      "data_json": {
        "animation": "victory",
        "duration": 2
      },
      "created_at": "2026-03-09 10:53:58"
    }
  ],
  "last_update_id": "f26cbcdab3939b968f148edf68a9fe54"
}
GET /v1/php/game_room.php/current?api_token=YOUR_API_TOKEN&game_player_token=YOUR_PLAYER_TOKEN

Description: Get current game room status and player information.

Request Body:
{}
Response:
{
  "success": true,
  "in_room": true,
  "room": {
    "room_id": "05f157893b1237f7699e548d045904ab",
    "room_name": "Game from Matchmaking 636b3f",
    "is_host": false,
    "is_online": true,
    "max_players": 4,
    "current_players": 2,
    "has_password": false,
    "is_active": true,
    "player_name": "TestPlayer",
    "joined_at": "2026-03-06 17:50:35",
    "last_heartbeat": "2026-03-06 18:02:35",
    "room_created_at": "2026-03-06 17:50:35",
    "room_last_activity": "2026-03-06 18:04:58"
  },
  "pending_actions": [],
  "pending_updates": []
}
POST /v1/php/leaderboard.php?api_token=YOUR_API_TOKEN

Description: Get leaderboard sorted by player level (descending).

Request Body:
{
  "sortBy": ["level"],
  "limit": 10
}
Response:
{
  "success": true,
  "leaderboard": [
    {
      "rank": 1,
      "player_id": 33,
      "player_name": "GameHost",
      "player_data": {
        "level": 15,
        "rank": "platinum",
        "role": "host",
        "last_played": "2026-03-13T14:28:06.9900113Z",
        "matchmaking_status": "ready"
      }
    },
    {
      "rank": 2,
      "player_id": 35,
      "player_name": "Player2",
      "player_data": {
        "level": 12,
        "rank": "gold",
        "role": "player"
      }
    },
    {
      "rank": 3,
      "player_id": 34,
      "player_name": "Player1",
      "player_data": {
        "level": 8,
        "rank": "silver",
        "role": "player"
      }
    },
    {
      "rank": 4,
      "player_id": 36,
      "player_name": "Player3",
      "player_data": {
        "level": 6,
        "rank": "bronze",
        "role": "player"
      }
    }
  ],
  "total": 4,
  "sort_by": ["level"],
  "limit": 10
}
POST /v1/php/leaderboard.php?api_token=YOUR_API_TOKEN

Description: Get leaderboard sorted by level first, then score as tie-breaker.

Request Body:
{
  "sortBy": ["level", "score"],
  "limit": 10
}
Response:
{
  "success": true,
  "leaderboard": [
    {
      "rank": 1,
      "player_id": 40,
      "player_name": "TestPlayer4",
      "player_data": {
        "level": 20,
        "score": 1000,
        "inventory": []
      }
    },
    {
      "rank": 2,
      "player_id": 37,
      "player_name": "TestPlayer1",
      "player_data": {
        "level": 15,
        "score": 4500,
        "inventory": ["sword"]
      }
    },
    {
      "rank": 3,
      "player_id": 38,
      "player_name": "TestPlayer2",
      "player_data": {
        "level": 15,
        "score": 2000,
        "inventory": ["sword", "shield"]
      }
    },
    {
      "rank": 4,
      "player_id": 41,
      "player_name": "TestPlayer5",
      "player_data": {
        "level": 12,
        "inventory": ["staff"]
      }
    },
    {
      "rank": 5,
      "player_id": 39,
      "player_name": "TestPlayer3",
      "player_data": {
        "level": 8,
        "score": 3000,
        "inventory": ["bow"]
      }
    }
  ],
  "total": 5,
  "sort_by": ["level", "score"],
  "limit": 10
}
POST /v1/php/leaderboard.php?api_token=YOUR_API_TOKEN

Description: Get leaderboard sorted by score first, then level as tie-breaker.

Request Body:
{
  "sortBy": ["score", "level"],
  "limit": 10
}
Response:
{
  "success": true,
  "leaderboard": [
    {
      "rank": 1,
      "player_id": 37,
      "player_name": "TestPlayer1",
      "player_data": {
        "level": 15,
        "score": 4500,
        "inventory": ["sword"]
      }
    },
    {
      "rank": 2,
      "player_id": 39,
      "player_name": "TestPlayer3",
      "player_data": {
        "level": 8,
        "score": 3000,
        "inventory": ["bow"]
      }
    },
    {
      "rank": 3,
      "player_id": 38,
      "player_name": "TestPlayer2",
      "player_data": {
        "level": 15,
        "score": 2000,
        "inventory": ["sword", "shield"]
      }
    },
    {
      "rank": 4,
      "player_id": 40,
      "player_name": "TestPlayer4",
      "player_data": {
        "level": 20,
        "score": 1000,
        "inventory": []
      }
    },
    {
      "rank": 5,
      "player_id": 41,
      "player_name": "TestPlayer5",
      "player_data": {
        "level": 12,
        "inventory": ["staff"]
      }
    }
  ],
  "total": 5,
  "sort_by": ["score", "level"],
  "limit": 10
}
Error Response (401 Unauthorized):

Description: Shows what happens when a request is sent with an invalid or missing API key.

Response:
{
    "success": false,
    "error": {
    "code": "unauthorized",
    "message": "Invalid or missing API key"
    }
}