Skip to main content
This guide shows how to integrate with PlayWave by calling HTTP APIs directly, without using the PlayWave Roblox SDK.

Integration flow

Prerequisites

  • API Key: Contact your PlayWave operations representative to request one
  • Roblox Studio: Game Settings → Security → Allow HTTP Requests enabled
  • Server-side only: All API calls must run in ServerScriptService

API Server URLs

EnvironmentURLStatus
Devhttps://wave-api.playwave.dev/v1Available
QAhttps://wave-api-qa.playwave.dev/v1Coming soon
Livehttps://wave-api.playwave.io/v1Coming soon

Authentication

All Game Server API requests require the X-Api-Key header.
X-Api-Key: {your_api_key}
Content-Type: application/json
API Key must only be used in ServerScriptService Scripts. Never place it in ReplicatedStorage, StarterPlayerScripts, LocalScript, or any client-accessible location.

OTT verification — POST /v1/game/session/verify

Call this first when a player joins the game. It verifies the OTT and creates a game session.

OTT extraction

The PlayWave launcher passes the OTT UUID directly via LaunchData.
local function extractOtt(player)
    local success, joinData = pcall(function()
        return player:GetJoinData()
    end)

    if not success or not joinData then
        return nil
    end

    local launchData = joinData.LaunchData
    if not launchData or type(launchData) ~= "string" or #launchData == 0 then
        return nil
    end

    return launchData
end

Request

local HttpService = game:GetService("HttpService")

local API_URL = "https://wave-api.playwave.dev/v1"
local API_KEY = "your-api-key-here"

local function verifySession(player)
    local ott = extractOtt(player)
    if not ott then
        return { is_valid = false, reason = "NO_OTT" }
    end

    local success, response = pcall(function()
        return HttpService:RequestAsync({
            Url = API_URL .. "/game/session/verify",
            Method = "POST",
            Headers = {
                ["X-Api-Key"] = API_KEY,
                ["Content-Type"] = "application/json",
            },
            Body = HttpService:JSONEncode({
                ott = ott,
                provider_user_id = tostring(player.UserId),
            }),
        })
    end)

    if not success then
        warn("[Playwave] HTTP request failed:", response)
        return { is_valid = false, reason = "NETWORK_ERROR" }
    end

    local decodeOk, body = pcall(function()
        return HttpService:JSONDecode(response.Body)
    end)

    if not decodeOk or not body.success or not body.data then
        return { is_valid = false, reason = "API_ERROR" }
    end

    return body.data
end

Success response

{
  "success": true,
  "request_id": "req_...",
  "data": {
    "is_valid": true,
    "game_session_id": "gs_550e8400-e29b-41d4-a716-446655440000",
    "is_pc_cafe": true
  }
}
FieldTypeDescription
is_validbooleanWhether verification succeeded
game_session_idstringGame session ID (used for heartbeat/end)
is_pc_cafebooleanWhether user is at a PC cafe

Failure response

{
  "success": true,
  "request_id": "req_...",
  "data": {
    "is_valid": false,
    "reason": "OTT_EXPIRED"
  }
}
Business logic failures also return HTTP 200. Always branch on the is_valid value.

reason values

reasonDescriptionGame server action
OTT_EXPIREDOTT expired (over 1 min since issuance)Tell user to relaunch from launcher
OTT_ALREADY_USEDOTT already consumedPrevent duplicate access — kick user
GAME_MISMATCHOTT game / API Key game mismatchInvalid OTT — kick user
NOT_CHARGEABLEBilling not possible (insufficient G-coin, etc.)Tell user to recharge G-coin

Heartbeat — PATCH /v1/game/session/heartbeat

Reports that the game session is still active. Must be called every 2 minutes (120 seconds). If heartbeats stop, the session is automatically deleted when the Redis TTL (4 min) expires.

Request

local function sendHeartbeat(gameSessionId, providerUserId)
    local success, response = pcall(function()
        return HttpService:RequestAsync({
            Url = API_URL .. "/game/session/heartbeat",
            Method = "PATCH",
            Headers = {
                ["X-Api-Key"] = API_KEY,
                ["Content-Type"] = "application/json",
            },
            Body = HttpService:JSONEncode({
                game_session_id = gameSessionId,
                provider_user_id = providerUserId,
            }),
        })
    end)

    if not success then
        warn("[Playwave] Heartbeat request failed:", response)
        return nil
    end

    local decodeOk, body = pcall(function()
        return HttpService:JSONDecode(response.Body)
    end)

    if not decodeOk then
        return nil
    end

    return body
end

Response

{
  "success": true,
  "request_id": "req_...",
  "data": {
    "result": "OK",
    "next_heartbeat_in": 120,
    "play_duration_sec": 1800
  }
}
FieldTypeDescription
resultstringSession status
next_heartbeat_inintegerRecommended wait time until next heartbeat (seconds)
play_duration_secintegerServer-side cumulative play time (seconds)

result values

resultDescriptionGame server action
OKNormalSend next heartbeat after next_heartbeat_in
CHARGE_EXHAUSTEDG-coin depletedNotify user. Server auto-terminates after 2 min grace

Error responses

HTTPCodeConditionGame server action
404SESSION_NOT_FOUNDSession not found (expired/deleted)Stop heartbeat, kick user
410SESSION_ENDEDSession already endedStop heartbeat, kick user
410SESSION_ENDINGSession termination in progressStop heartbeat, kick user

Session end — DELETE /v1/game/session/end

Call when a player leaves the game. Stops billing and cleans up the session.

Request

local function endSession(gameSessionId, playDurationSec)
    local requestBody = {
        game_session_id = gameSessionId,
    }

    if playDurationSec then
        requestBody.play_duration_sec = playDurationSec
    end

    local success, response = pcall(function()
        return HttpService:RequestAsync({
            Url = API_URL .. "/game/session/end",
            Method = "DELETE",
            Headers = {
                ["X-Api-Key"] = API_KEY,
                ["Content-Type"] = "application/json",
            },
            Body = HttpService:JSONEncode(requestBody),
        })
    end)

    if not success then
        warn("[Playwave] End session request failed:", response)
        return nil
    end

    local decodeOk, body = pcall(function()
        return HttpService:JSONDecode(response.Body)
    end)

    if not decodeOk then
        return nil
    end

    return body
end

Response

{
  "success": true,
  "request_id": "req_...",
  "data": {
    "result": "SUCCESS",
    "play_duration_sec": 1800,
    "client_play_duration_sec": 1795
  }
}
FieldTypeDescription
resultstringAlways "SUCCESS"
play_duration_secintegerServer-side play time (seconds) — used for settlement
client_play_duration_secinteger | nullValue sent in request. null if not provided

Error responses

HTTPCodeConditionGame server action
404SESSION_NOT_FOUNDSession not found (already expired/deleted)Ignore (already cleaned up)
409ALREADY_ENDEDSession already endedIgnore (duplicate call)

Full implementation example

Add this Script to ServerScriptService.
-- ServerScriptService/PlaywaveIntegration (Script)

local Players = game:GetService("Players")
local HttpService = game:GetService("HttpService")

-----------------------------------------------------------------
-- Configuration
-----------------------------------------------------------------
local API_URL = "https://wave-api.playwave.dev/v1"
local API_KEY = "your-api-key-here"

local HEARTBEAT_INTERVAL = 120

-----------------------------------------------------------------
-- Per-player session data
-----------------------------------------------------------------
local activeSessions = {}

-----------------------------------------------------------------
-- OTT extraction
-----------------------------------------------------------------
local function extractOtt(player)
    local success, joinData = pcall(function()
        return player:GetJoinData()
    end)

    if not success or not joinData then
        return nil
    end

    local launchData = joinData.LaunchData
    if not launchData or type(launchData) ~= "string" then
        return nil
    end

    return launchData
end

-----------------------------------------------------------------
-- API request helper
-----------------------------------------------------------------
local function apiRequest(method, path, body)
    local requestInfo = {
        Url = API_URL .. path,
        Method = method,
        Headers = {
            ["X-Api-Key"] = API_KEY,
            ["Content-Type"] = "application/json",
        },
    }

    if body then
        requestInfo.Body = HttpService:JSONEncode(body)
    end

    local success, response = pcall(function()
        return HttpService:RequestAsync(requestInfo)
    end)

    if not success then
        warn("[Playwave] HTTP request failed:", response)
        return nil, "NETWORK_ERROR"
    end

    if response.StatusCode >= 400 then
        local errorBody = nil
        pcall(function()
            errorBody = HttpService:JSONDecode(response.Body)
        end)
        return errorBody, "HTTP_" .. tostring(response.StatusCode)
    end

    local decodeSuccess, decoded = pcall(function()
        return HttpService:JSONDecode(response.Body)
    end)

    if not decodeSuccess then
        warn("[Playwave] JSON decode failed:", decoded)
        return nil, "DECODE_ERROR"
    end

    return decoded, nil
end

-----------------------------------------------------------------
-- OTT verification + game session creation
-----------------------------------------------------------------
local function verifySession(player)
    local ott = extractOtt(player)
    if not ott then
        return nil
    end

    local result, err = apiRequest("POST", "/game/session/verify", {
        ott = ott,
        provider_user_id = tostring(player.UserId),
    })

    if err then
        warn("[Playwave] Verify failed for", player.Name, ":", err)
        return nil
    end

    if result and result.success and result.data then
        return result.data
    end

    return nil
end

-----------------------------------------------------------------
-- Heartbeat loop
-----------------------------------------------------------------
local function startHeartbeatLoop(userId, gameSessionId, providerUserId)
    return task.spawn(function()
        while activeSessions[userId] do
            task.wait(HEARTBEAT_INTERVAL)

            if not activeSessions[userId] then
                break
            end

            local result, err = apiRequest("PATCH", "/game/session/heartbeat", {
                game_session_id = gameSessionId,
                provider_user_id = providerUserId,
            })

            if err then
                if err == "HTTP_404" or err == "HTTP_410" then
                    warn("[Playwave] Session lost for UserId", userId, ":", err)
                    activeSessions[userId] = nil
                    local player = Players:GetPlayerByUserId(userId)
                    if player then
                        player:Kick("PlayWave session expired")
                    end
                    break
                end
                warn("[Playwave] Heartbeat error for UserId", userId, ":", err)
                continue
            end

            if result and result.data then
                local hbResult = result.data.result

                if hbResult == "CHARGE_EXHAUSTED" then
                    local player = Players:GetPlayerByUserId(userId)
                    if player then
                        warn("[Playwave] Charge exhausted for", player.Name)
                    end
                end
            end
        end
    end)
end

-----------------------------------------------------------------
-- Session end
-----------------------------------------------------------------
local function endPlayerSession(userId)
    local session = activeSessions[userId]
    if not session then
        return
    end

    activeSessions[userId] = nil

    if session.heartbeatThread then
        task.cancel(session.heartbeatThread)
    end

    local clientPlayDuration = math.floor(os.clock() - session.startTime)

    local result, err = apiRequest("DELETE", "/game/session/end", {
        game_session_id = session.gameSessionId,
        play_duration_sec = clientPlayDuration,
    })

    if err then
        if err ~= "HTTP_404" and err ~= "HTTP_409" then
            warn("[Playwave] End session failed for UserId", userId, ":", err)
        end
    end
end

-----------------------------------------------------------------
-- Event connections
-----------------------------------------------------------------
Players.PlayerAdded:Connect(function(player)
    local data = verifySession(player)

    if not data or not data.is_valid then
        if data and data.reason then
            warn("[Playwave] Verify failed:", player.Name, data.reason)
        end
        return
    end

    local providerUserId = tostring(player.UserId)
    activeSessions[player.UserId] = {
        gameSessionId = data.game_session_id,
        startTime = os.clock(),
        heartbeatThread = nil,
    }

    activeSessions[player.UserId].heartbeatThread = startHeartbeatLoop(
        player.UserId,
        data.game_session_id,
        providerUserId
    )

    -- Grant PC cafe benefits (implement per game)
    if data.is_pc_cafe then
        grantPcCafeBonus(player)
    end
end)

Players.PlayerRemoving:Connect(function(player)
    endPlayerSession(player.UserId)
end)

game:BindToClose(function()
    for userId, _ in pairs(activeSessions) do
        endPlayerSession(userId)
    end
end)

-----------------------------------------------------------------
-- Per-game benefit logic (implement yourself)
-----------------------------------------------------------------
function grantPcCafeBonus(player)
    -- Example: XP boost, bonus coins, exclusive items, etc.
    print("[Playwave] PC cafe bonus granted to", player.Name)
end

Security notes

API Key protection

  • API Key must only be used in ServerScriptService Scripts
  • Never place in ReplicatedStorage, StarterPlayerScripts, LocalScript, or any client-accessible location
  • Never pass API Key to client via RemoteEvent/RemoteFunction

HttpService

  • HttpService:RequestAsync() can only be called from ServerScripts (Roblox security policy)
  • Game Settings → Security → Allow HTTP Requests must be enabled

OTT

  • OTT is single-use — consumed immediately on verify call, cannot be reused
  • Must be used within 1 minute (60 seconds) of issuance
  • OTT format: UUID (e.g., a1b2c3d4-e5f6-7890-abcd-ef1234567890)