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
| Environment | URL | Status |
|---|---|---|
| Dev | https://wave-api.playwave.dev/v1 | Available |
| QA | https://wave-api-qa.playwave.dev/v1 | Coming soon |
| Live | https://wave-api.playwave.io/v1 | Coming soon |
Authentication
All Game Server API requests require theX-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 viaLaunchData.
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
}
}
| Field | Type | Description |
|---|---|---|
is_valid | boolean | Whether verification succeeded |
game_session_id | string | Game session ID (used for heartbeat/end) |
is_pc_cafe | boolean | Whether 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
| reason | Description | Game server action |
|---|---|---|
OTT_EXPIRED | OTT expired (over 1 min since issuance) | Tell user to relaunch from launcher |
OTT_ALREADY_USED | OTT already consumed | Prevent duplicate access — kick user |
GAME_MISMATCH | OTT game / API Key game mismatch | Invalid OTT — kick user |
NOT_CHARGEABLE | Billing 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
}
}
| Field | Type | Description |
|---|---|---|
result | string | Session status |
next_heartbeat_in | integer | Recommended wait time until next heartbeat (seconds) |
play_duration_sec | integer | Server-side cumulative play time (seconds) |
result values
| result | Description | Game server action |
|---|---|---|
OK | Normal | Send next heartbeat after next_heartbeat_in |
CHARGE_EXHAUSTED | G-coin depleted | Notify user. Server auto-terminates after 2 min grace |
Error responses
| HTTP | Code | Condition | Game server action |
|---|---|---|---|
| 404 | SESSION_NOT_FOUND | Session not found (expired/deleted) | Stop heartbeat, kick user |
| 410 | SESSION_ENDED | Session already ended | Stop heartbeat, kick user |
| 410 | SESSION_ENDING | Session termination in progress | Stop 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
}
}
| Field | Type | Description |
|---|---|---|
result | string | Always "SUCCESS" |
play_duration_sec | integer | Server-side play time (seconds) — used for settlement |
client_play_duration_sec | integer | null | Value sent in request. null if not provided |
Error responses
| HTTP | Code | Condition | Game server action |
|---|---|---|---|
| 404 | SESSION_NOT_FOUND | Session not found (already expired/deleted) | Ignore (already cleaned up) |
| 409 | ALREADY_ENDED | Session already ended | Ignore (duplicate call) |
Full implementation example
Add this Script toServerScriptService.
-- 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
ServerScriptServiceScripts - 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)