메인 콘텐츠로 건너뛰기
PlayWave Roblox SDK 없이 HTTP API를 직접 호출하여 연동하는 방법을 안내합니다.

연동 흐름

전제조건

  • API Key 발급: PlayWave 서비스 운영 담당자에게 요청
  • Roblox Studio: Game Settings → Security → Allow HTTP Requests 활성화
  • 서버 사이드 전용: 모든 API 호출은 ServerScriptService에서 실행

API 서버 주소

환경URL상태
Devhttps://wave-api.playwave.dev/v1사용 가능
QAhttps://wave-api-qa.playwave.dev/v1준비 중
Livehttps://wave-api.playwave.io/v1준비 중

인증

모든 Game Server API 요청에는 X-Api-Key 헤더가 필요합니다.
X-Api-Key: {발급받은_api_key}
Content-Type: application/json
API Key는 반드시 ServerScriptService의 Script에서만 사용하세요. ReplicatedStorage, StarterPlayerScripts, LocalScript 등 클라이언트에서 접근 가능한 곳에 절대 배치하지 마세요.

OTT 검증 — POST /v1/game/session/verify

플레이어가 게임에 접속하면 가장 먼저 호출합니다. OTT를 검증하고 게임 세션을 생성합니다.

OTT 추출

PlayWave 런처는 게임 실행 시 LaunchData에 OTT UUID를 직접 전달합니다.
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

요청

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": true,
  "request_id": "req_...",
  "data": {
    "is_valid": true,
    "game_session_id": "gs_550e8400-e29b-41d4-a716-446655440000",
    "is_pc_cafe": true
  }
}
필드타입설명
is_validboolean검증 성공 여부
game_session_idstring게임 세션 ID (하트비트/종료에 사용)
is_pc_cafebooleanPC방 여부

실패 응답

{
  "success": true,
  "request_id": "req_...",
  "data": {
    "is_valid": false,
    "reason": "OTT_EXPIRED"
  }
}
비즈니스 로직 실패도 HTTP 200으로 반환됩니다. 반드시 is_valid 값으로 분기하세요.

reason 값과 처리

reason설명게임 서버 처리
OTT_EXPIREDOTT 만료 (발급 후 1분 초과)유저에게 런처로 돌아가서 재실행 안내
OTT_ALREADY_USEDOTT 이미 사용됨중복 접속 방지 — 유저 킥
GAME_MISMATCHOTT의 게임과 API Key의 게임 불일치잘못된 OTT — 유저 킥
NOT_CHARGEABLE과금 불가 (G-coin 부족 등)유저에게 G-coin 충전 안내

하트비트 — PATCH /v1/game/session/heartbeat

게임 세션이 활성 상태임을 서버에 알립니다. 2분(120초) 간격으로 호출해야 합니다. 하트비트를 보내지 않으면 Redis TTL(4분) 만료로 세션이 자동 삭제됩니다.

요청

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

응답

{
  "success": true,
  "request_id": "req_...",
  "data": {
    "result": "OK",
    "next_heartbeat_in": 120,
    "play_duration_sec": 1800
  }
}
필드타입설명
resultstring세션 상태
next_heartbeat_ininteger다음 하트비트까지 권장 대기 시간 (초)
play_duration_secinteger서버 기준 누적 플레이 시간 (초)

result 값과 처리

result설명게임 서버 처리
OK정상next_heartbeat_in 후 다음 하트비트 전송
CHARGE_EXHAUSTEDG-coin 소진유저에게 안내. 서버가 2분 유예 후 자동 종료

에러 응답

HTTP코드조건게임 서버 처리
404SESSION_NOT_FOUND세션 없음 (만료/삭제)하트비트 중지, 유저 킥
410SESSION_ENDED이미 종료된 세션하트비트 중지, 유저 킥
410SESSION_ENDING종료 진행 중하트비트 중지, 유저 킥

세션 종료 — DELETE /v1/game/session/end

플레이어가 게임을 떠날 때 호출합니다. 과금을 중지하고 세션을 정리합니다.

요청

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

응답

{
  "success": true,
  "request_id": "req_...",
  "data": {
    "result": "SUCCESS",
    "play_duration_sec": 1800,
    "client_play_duration_sec": 1795
  }
}
필드타입설명
resultstring항상 "SUCCESS"
play_duration_secinteger서버 기준 플레이 시간 (초) — 정산에 사용
client_play_duration_secinteger | null요청에서 보낸 값. 미전달 시 null

에러 응답

HTTP코드조건게임 서버 처리
404SESSION_NOT_FOUND세션 없음 (이미 만료/삭제)무시 (이미 정리됨)
409ALREADY_ENDED이미 종료된 세션무시 (중복 호출)

전체 구현 예제

ServerScriptService에 아래 Script를 추가합니다.
-- ServerScriptService/PlaywaveIntegration (Script)

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

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

local HEARTBEAT_INTERVAL = 120

-----------------------------------------------------------------
-- 플레이어별 세션 데이터
-----------------------------------------------------------------
local activeSessions = {}

-----------------------------------------------------------------
-- OTT 추출
-----------------------------------------------------------------
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 호출 헬퍼
-----------------------------------------------------------------
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 검증 + 게임 세션 생성
-----------------------------------------------------------------
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

-----------------------------------------------------------------
-- 하트비트 루프
-----------------------------------------------------------------
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

-----------------------------------------------------------------
-- 세션 종료
-----------------------------------------------------------------
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

-----------------------------------------------------------------
-- 이벤트 연결
-----------------------------------------------------------------
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
    )

    -- PC방 혜택 부여 (게임별 구현)
    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)

-----------------------------------------------------------------
-- 게임별 혜택 로직 (직접 구현)
-----------------------------------------------------------------
function grantPcCafeBonus(player)
    -- 예: XP 부스트, 보너스 코인, 전용 아이템 등
    print("[Playwave] PC cafe bonus granted to", player.Name)
end

보안 주의사항

API Key 보호

  • API Key는 반드시 ServerScriptService의 Script에서만 사용
  • ReplicatedStorage, StarterPlayerScripts, LocalScript 등 클라이언트 접근 가능한 곳에 절대 배치 금지
  • RemoteEvent/RemoteFunction을 통해 API Key를 클라이언트에 전달 금지

HttpService

  • HttpService:RequestAsync()ServerScript에서만 호출 가능 (Roblox 보안 정책)
  • Game Settings → Security → Allow HTTP Requests를 반드시 활성화

OTT

  • OTT는 1회용 — verify 호출 시 즉시 소비, 재사용 불가
  • 발급 후 1분(60초) 이내에 사용해야 함
  • OTT 형식: UUID (예: a1b2c3d4-e5f6-7890-abcd-ef1234567890)