local HttpService = game:GetService("HttpService") local Players = game:GetService("Players") local function hexToColor3(value, fallback) local color = tostring(value or "") local r, g, b = color:match("^#?(%x%x)(%x%x)(%x%x)$") if not r then return fallback or Color3.fromRGB(32, 227, 178) end return Color3.fromRGB(tonumber(r, 16), tonumber(g, 16), tonumber(b, 16)) end local RobloxAPIs = {} RobloxAPIs.__index = RobloxAPIs function RobloxAPIs.new(config) if type(config) == "string" then config = { apiKey = config } end config = config or {} assert(config.apiKey, "RobloxAPIs.new requires an apiKey") return setmetatable({ BaseUrl = config.baseUrl or "https://robloxapis.com", ApiKey = config.apiKey, Timeout = config.timeout or 15 }, RobloxAPIs) end function RobloxAPIs:Request(path, method, body) local request = { Url = self.BaseUrl .. path, Method = method or "GET", Headers = { ["Authorization"] = "Bearer " .. self.ApiKey, ["Content-Type"] = "application/json" } } if body ~= nil then request.Body = HttpService:JSONEncode(body) end local response = HttpService:RequestAsync(request) local decoded = nil if response.Body and response.Body ~= "" then local ok, result = pcall(function() return HttpService:JSONDecode(response.Body) end) decoded = ok and result or { rawBody = response.Body } end if not response.Success then return nil, { statusCode = response.StatusCode, statusMessage = response.StatusMessage, body = decoded } end return decoded, nil end function RobloxAPIs:CheckBan(userId, context) context = context or {} context.userId = userId return self:Request("/api/v1/cross-ban/check", "POST", context) end function RobloxAPIs:BanUser(userId, options) options = options or {} options.userId = userId return self:Request("/api/v1/cross-ban/bans", "POST", options) end function RobloxAPIs:RevokeBan(banId) return self:Request("/api/v1/cross-ban/bans/" .. HttpService:UrlEncode(tostring(banId)), "DELETE") end function RobloxAPIs:RunCrossBanSelfTest(testUserId, context) context = context or {} testUserId = testUserId or context.userId or 156 local testUniverseId = context.universeId or context.scopeId or tonumber(tostring(os.time()) .. tostring(math.random(1000, 9999))) local created, createErr = self:BanUser(testUserId, { scopeType = context.scopeType or "universe", scopeId = tostring(testUniverseId), reason = context.reason or "RobloxAPIs SDK self-test", note = "Temporary test ban created by RunCrossBanSelfTest" }) if createErr then return nil, createErr end local check, checkErr = self:CheckBan(testUserId, { universeId = testUniverseId, groupId = context.groupId, serverJobId = context.serverJobId or game.JobId }) local revoked = nil local revokeErr = nil if created and created.ban and created.ban.id then revoked, revokeErr = self:RevokeBan(created.ban.id) end return { ok = check ~= nil and check.banned == true, testUniverseId = testUniverseId, created = created, check = check, revoked = revoked, errors = { check = checkErr, revoke = revokeErr } }, nil end function RobloxAPIs:GetPlayerIntel(userId) return self:Request("/api/v1/intel/users/" .. tostring(userId), "GET") end function RobloxAPIs:PutObject(objectKey, value) return self:Request("/api/v1/vault/objects/" .. HttpService:UrlEncode(objectKey), "PUT", { value = value }) end function RobloxAPIs:GetObject(objectKey) return self:Request("/api/v1/vault/objects/" .. HttpService:UrlEncode(objectKey), "GET") end function RobloxAPIs:DeleteObject(objectKey) return self:Request("/api/v1/vault/objects/" .. HttpService:UrlEncode(objectKey), "DELETE") end function RobloxAPIs:SetExperienceConfig(configKey, config) if type(configKey) == "table" then config = configKey configKey = "default" end return self:Request("/api/v1/experience/configs/" .. HttpService:UrlEncode(tostring(configKey or "default")), "PUT", config or {}) end function RobloxAPIs:GetExperienceConfig(configKey) return self:Request("/api/v1/experience/configs/" .. HttpService:UrlEncode(tostring(configKey or "default")), "GET") end function RobloxAPIs:ApplyExperienceConfigSign(configKey, options) options = options or {} local result, err = self:GetExperienceConfig(configKey or options.configKey or "default") if err then return nil, err end local config = result.config or {} local partName = options.partName or "RobloxAPIs_LiveConfig" local part = workspace:FindFirstChild(partName) if not part then part = Instance.new("Part") part.Name = partName part.Anchored = true part.Size = options.size or Vector3.new(16, 9, 1) part.Position = options.position or Vector3.new(0, 8, -14) part.Parent = workspace end local surface = part:FindFirstChild("RobloxAPIs_Surface") if not surface then surface = Instance.new("SurfaceGui") surface.Name = "RobloxAPIs_Surface" surface.Face = Enum.NormalId.Front surface.CanvasSize = Vector2.new(1200, 700) surface.Parent = part end local label = surface:FindFirstChild("Message") if not label then label = Instance.new("TextLabel") label.Name = "Message" label.Size = UDim2.fromScale(1, 1) label.BorderSizePixel = 0 label.TextScaled = true label.Font = Enum.Font.GothamBlack label.TextColor3 = Color3.fromRGB(245, 248, 255) label.Parent = surface end part.Color = hexToColor3(config.accent, Color3.fromRGB(32, 227, 178)) label.BackgroundColor3 = Color3.fromRGB(5, 6, 8) label.BackgroundTransparency = 0.08 label.Text = tostring(config.title or "Roblox APIs") .. "\n" .. tostring(config.message or "") return { ok = true, result = result, part = part, label = label }, nil end function RobloxAPIs:SendEconomySignal(userId, action, valueDelta, metadata) return self:Request("/api/v1/economy/signals", "POST", { userId = userId, action = action, valueDelta = valueDelta or 0, metadata = metadata or {} }) end function RobloxAPIs:TrackAnalyticsEvent(event) return self:Request("/api/v1/analytics/events", "POST", event or {}) end function RobloxAPIs:TrackAnalyticsEvents(events) return self:Request("/api/v1/analytics/events", "POST", { events = events or {} }) end function RobloxAPIs:GetAnalyticsSummary(options) options = options or {} local query = {} if options.eventType ~= nil then table.insert(query, "eventType=" .. HttpService:UrlEncode(tostring(options.eventType))) elseif options.type ~= nil then table.insert(query, "eventType=" .. HttpService:UrlEncode(tostring(options.type))) end if options.sessionId ~= nil then table.insert(query, "sessionId=" .. HttpService:UrlEncode(tostring(options.sessionId))) end if options.userId ~= nil then table.insert(query, "userId=" .. HttpService:UrlEncode(tostring(options.userId))) end if options.universeId ~= nil then table.insert(query, "universeId=" .. HttpService:UrlEncode(tostring(options.universeId))) elseif options.gameId ~= nil then table.insert(query, "universeId=" .. HttpService:UrlEncode(tostring(options.gameId))) end if options.placeId ~= nil then table.insert(query, "placeId=" .. HttpService:UrlEncode(tostring(options.placeId))) end if options.serverJobId ~= nil then table.insert(query, "serverJobId=" .. HttpService:UrlEncode(tostring(options.serverJobId))) elseif options.jobId ~= nil then table.insert(query, "serverJobId=" .. HttpService:UrlEncode(tostring(options.jobId))) end if options.sinceMinutes ~= nil then table.insert(query, "sinceMinutes=" .. HttpService:UrlEncode(tostring(options.sinceMinutes))) end if options.limit ~= nil then table.insert(query, "limit=" .. HttpService:UrlEncode(tostring(options.limit))) end local path = "/api/v1/analytics/summary" if #query > 0 then path = path .. "?" .. table.concat(query, "&") end return self:Request(path, "GET") end function RobloxAPIs:ListAnalyticsEvents(userId, options) options = options or {} local query = {} if options.eventType ~= nil then table.insert(query, "eventType=" .. HttpService:UrlEncode(tostring(options.eventType))) elseif options.type ~= nil then table.insert(query, "eventType=" .. HttpService:UrlEncode(tostring(options.type))) end if options.sessionId ~= nil then table.insert(query, "sessionId=" .. HttpService:UrlEncode(tostring(options.sessionId))) end if options.sinceMinutes ~= nil then table.insert(query, "sinceMinutes=" .. HttpService:UrlEncode(tostring(options.sinceMinutes))) end if options.limit ~= nil then table.insert(query, "limit=" .. HttpService:UrlEncode(tostring(options.limit))) end local path = "/api/v1/analytics/users/" .. tostring(userId) .. "/events" if #query > 0 then path = path .. "?" .. table.concat(query, "&") end return self:Request(path, "GET") end function RobloxAPIs:DeleteAnalyticsEvent(eventId) return self:Request("/api/v1/analytics/events/" .. HttpService:UrlEncode(tostring(eventId)), "DELETE") end function RobloxAPIs:RunAnalyticsSelfTest(userId) userId = userId or 156 local stamp = tostring(os.time()) .. "_" .. tostring(math.random(1000, 9999)) local sessionId = "sdk_session_" .. stamp local firstEventId = "sdk_analytics_start_" .. stamp local secondEventId = "sdk_analytics_round_" .. stamp local tracked, trackErr = self:TrackAnalyticsEvents({ { eventId = firstEventId, userId = userId, sessionId = sessionId, eventType = "session_started", value = 1, properties = { source = "RunAnalyticsSelfTest" } }, { eventId = secondEventId, userId = userId, sessionId = sessionId, eventType = "round_completed", value = 120, properties = { result = "win", source = "RunAnalyticsSelfTest" } } }) if trackErr then return nil, trackErr end local summary, summaryErr = self:GetAnalyticsSummary({ sessionId = sessionId, sinceMinutes = 60, limit = 20 }) local listed, listErr = self:ListAnalyticsEvents(userId, { sessionId = sessionId, sinceMinutes = 60, limit = 20 }) local deletedFirst, deleteFirstErr = self:DeleteAnalyticsEvent(firstEventId) local deletedSecond, deleteSecondErr = self:DeleteAnalyticsEvent(secondEventId) local foundRound = false if listed and listed.events then for _, event in ipairs(listed.events) do if event.eventId == secondEventId then foundRound = true end end end return { ok = tracked ~= nil and tracked.accepted == 2 and summary ~= nil and summary.totals ~= nil and summary.totals.events >= 2 and foundRound and deletedFirst ~= nil and deletedFirst.deleted == true and deletedSecond ~= nil and deletedSecond.deleted == true, sessionId = sessionId, tracked = tracked, summary = summary, listed = listed, deleted = { deletedFirst, deletedSecond }, errors = { summary = summaryErr, list = listErr, deleteFirst = deleteFirstErr, deleteSecond = deleteSecondErr } }, nil end function RobloxAPIs:ListFeatureFlags(options) options = options or {} local query = {} if options.enabled ~= nil then table.insert(query, "enabled=" .. (options.enabled and "1" or "0")) end if options.limit ~= nil then table.insert(query, "limit=" .. HttpService:UrlEncode(tostring(options.limit))) end local path = "/api/v1/flags" if #query > 0 then path = path .. "?" .. table.concat(query, "&") end return self:Request(path, "GET") end function RobloxAPIs:SetFeatureFlag(flagKey, flag) return self:Request("/api/v1/flags/" .. HttpService:UrlEncode(tostring(flagKey)), "PUT", flag or {}) end function RobloxAPIs:GetFeatureFlag(flagKey) return self:Request("/api/v1/flags/" .. HttpService:UrlEncode(tostring(flagKey)), "GET") end function RobloxAPIs:DeleteFeatureFlag(flagKey) return self:Request("/api/v1/flags/" .. HttpService:UrlEncode(tostring(flagKey)), "DELETE") end function RobloxAPIs:GetFeatureAssignment(flagKey, userId) return self:Request("/api/v1/flags/" .. HttpService:UrlEncode(tostring(flagKey)) .. "/assignments/" .. tostring(userId), "GET") end function RobloxAPIs:TrackFeatureExposure(flagKey, exposure) return self:Request("/api/v1/flags/" .. HttpService:UrlEncode(tostring(flagKey)) .. "/exposures", "POST", exposure or {}) end function RobloxAPIs:RunFeatureFlagsSelfTest(userId) userId = userId or 156 local stamp = tostring(os.time()) .. "_" .. tostring(math.random(1000, 9999)) local flagKey = "sdk-experiment-" .. stamp local exposureId = "sdk_exposure_" .. stamp local saved, saveErr = self:SetFeatureFlag(flagKey, { description = "SDK self-test experiment flag", enabled = true, defaultVariant = "control", rolloutPercent = 100, variants = { { key = "control", weight = 50, payload = { multiplier = 1 } }, { key = "treatment", weight = 50, payload = { multiplier = 2 } } }, metadata = { source = "RunFeatureFlagsSelfTest" } }) if saveErr then return nil, saveErr end local assignment, assignmentErr = self:GetFeatureAssignment(flagKey, userId) local assignedVariant = assignment and assignment.assignment and assignment.assignment.variant or "control" local exposure, exposureErr = self:TrackFeatureExposure(flagKey, { exposureId = exposureId, userId = userId, variant = assignedVariant, sessionId = "sdk-session-" .. stamp, metadata = { source = "RunFeatureFlagsSelfTest" } }) local listed, listErr = self:ListFeatureFlags({ enabled = true, limit = 100 }) local fetched, fetchErr = self:GetFeatureFlag(flagKey) local deleted, deleteErr = self:DeleteFeatureFlag(flagKey) local found = false if listed and listed.flags then for _, flag in ipairs(listed.flags) do if flag.flagKey == flagKey then found = true end end end return { ok = saved ~= nil and saved.flag ~= nil and assignment ~= nil and assignment.assignment ~= nil and assignment.assignment.variant ~= nil and exposure ~= nil and exposure.duplicate == false and found and fetched ~= nil and fetched.found == true and deleted ~= nil and deleted.deleted == true, flagKey = flagKey, exposureId = exposureId, saved = saved, assignment = assignment, exposure = exposure, listed = listed, fetched = fetched, deleted = deleted, errors = { assignment = assignmentErr, exposure = exposureErr, list = listErr, fetch = fetchErr, delete = deleteErr } }, nil end function RobloxAPIs:VerifyTrade(trade, options) trade = trade or {} options = options or {} return self:Request("/api/v1/trades/verify", "POST", { tradeId = options.tradeId or trade.tradeId or trade.id, fromUserId = options.fromUserId or trade.fromUserId or trade.senderUserId or trade.initiatorUserId, toUserId = options.toUserId or trade.toUserId or trade.receiverUserId or trade.targetUserId, offeredItems = options.offeredItems or trade.offeredItems or trade.offered or trade.give or {}, requestedItems = options.requestedItems or trade.requestedItems or trade.receivedItems or trade.requested or trade.receive or {}, offeredValue = options.offeredValue or trade.offeredValue, requestedValue = options.requestedValue or trade.requestedValue, valueDelta = options.valueDelta or trade.valueDelta, universeId = options.universeId or game.GameId, metadata = options.metadata or trade.metadata or {} }) end function RobloxAPIs:GetTrade(tradeId) return self:Request("/api/v1/trades/" .. HttpService:UrlEncode(tostring(tradeId)), "GET") end function RobloxAPIs:RunTradeSelfTest(fromUserId, toUserId) fromUserId = fromUserId or 156 toUserId = toUserId or 1 local tradeId = "sdk_trade_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000, 9999)) local trade = { tradeId = tradeId, fromUserId = fromUserId, toUserId = toUserId, offeredItems = { { itemId = "crystal_sword", name = "Crystal Sword", quantity = 1, value = 1200, rarity = "rare" } }, requestedItems = { { itemId = "gold_pack", name = "Gold Pack", quantity = 1, value = 900, rarity = "uncommon" } }, metadata = { source = "RunTradeSelfTest", velocity = 1 } } local first, firstErr = self:VerifyTrade(trade) if firstErr then return nil, firstErr end local duplicate, duplicateErr = self:VerifyTrade(trade) local lookup, lookupErr = self:GetTrade(tradeId) return { ok = first ~= nil and first.duplicate == false and duplicate ~= nil and duplicate.duplicate == true and lookup ~= nil and lookup.trade ~= nil, tradeId = tradeId, first = first, duplicate = duplicate, lookup = lookup, errors = { duplicate = duplicateErr, lookup = lookupErr } }, nil end function RobloxAPIs:VerifyReceipt(receipt, options) options = options or {} receipt = receipt or {} return self:Request("/api/v1/receipts/verify", "POST", { receiptId = options.receiptId or options.purchaseId or receipt.PurchaseId or receipt.purchaseId, userId = options.userId or receipt.PlayerId or receipt.userId, productId = options.productId or receipt.ProductId or receipt.productId, grant = options.grant or options.entitlement or receipt.Grant or receipt.grant, universeId = options.universeId or game.GameId, placeId = options.placeId or game.PlaceId, metadata = options.metadata or {} }) end function RobloxAPIs:GetReceipt(receiptId) return self:Request("/api/v1/receipts/" .. HttpService:UrlEncode(tostring(receiptId)), "GET") end function RobloxAPIs:RunReceiptSelfTest(userId, productId, grant) local receiptId = "sdk_test_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000, 9999)) local first, firstErr = self:VerifyReceipt({ PurchaseId = receiptId, PlayerId = userId or 156, ProductId = productId or 100001 }, { grant = grant or "sdk_test_entitlement", metadata = { source = "RunReceiptSelfTest" } }) if firstErr then return nil, firstErr end local duplicate, duplicateErr = self:VerifyReceipt({ PurchaseId = receiptId, PlayerId = userId or 156, ProductId = productId or 100001 }, { grant = grant or "sdk_test_entitlement", metadata = { source = "RunReceiptSelfTest duplicate" } }) return { ok = first ~= nil and first.duplicate == false and duplicate ~= nil and duplicate.duplicate == true, receiptId = receiptId, first = first, duplicate = duplicate, errors = { duplicate = duplicateErr } }, nil end function RobloxAPIs:GrantEntitlement(userId, entitlementId, options) options = options or {} return self:Request("/api/v1/entitlements/grant", "POST", { userId = userId, entitlementId = entitlementId, type = options.type or options.entitlementType or "custom", source = options.source or "manual", scopeType = options.scopeType, scopeId = options.scopeId, universeId = options.universeId or game.GameId, placeId = options.placeId, groupId = options.groupId, quantity = options.quantity or 1, status = options.status or "active", expiresAt = options.expiresAt, metadata = options.metadata or {} }) end function RobloxAPIs:CheckEntitlements(userId, entitlements, options) options = options or {} local payloadEntitlements = entitlements if payloadEntitlements == nil then payloadEntitlements = options.entitlements or options.entitlementIds or options.entitlementId end return self:Request("/api/v1/entitlements/check", "POST", { userId = userId, entitlements = payloadEntitlements, mode = options.mode or "any", universeId = options.universeId or game.GameId, placeId = options.placeId or game.PlaceId, groupId = options.groupId }) end function RobloxAPIs:ListEntitlements(userId, options) options = options or {} local query = {} if options.status ~= nil then table.insert(query, "status=" .. HttpService:UrlEncode(tostring(options.status))) end if options.type ~= nil then table.insert(query, "type=" .. HttpService:UrlEncode(tostring(options.type))) end if options.entitlementId ~= nil then table.insert(query, "entitlementId=" .. HttpService:UrlEncode(tostring(options.entitlementId))) end if options.scopeType ~= nil then table.insert(query, "scopeType=" .. HttpService:UrlEncode(tostring(options.scopeType))) end if options.scopeId ~= nil then table.insert(query, "scopeId=" .. HttpService:UrlEncode(tostring(options.scopeId))) end if options.universeId ~= nil then table.insert(query, "universeId=" .. HttpService:UrlEncode(tostring(options.universeId))) end if options.placeId ~= nil then table.insert(query, "placeId=" .. HttpService:UrlEncode(tostring(options.placeId))) end if options.groupId ~= nil then table.insert(query, "groupId=" .. HttpService:UrlEncode(tostring(options.groupId))) end if options.includeRevoked ~= nil then table.insert(query, "includeRevoked=" .. HttpService:UrlEncode(options.includeRevoked and "1" or "0")) end if options.limit ~= nil then table.insert(query, "limit=" .. HttpService:UrlEncode(tostring(options.limit))) end local path = "/api/v1/entitlements/users/" .. tostring(userId) if #query > 0 then path = path .. "?" .. table.concat(query, "&") end return self:Request(path, "GET") end function RobloxAPIs:RevokeEntitlement(userId, entitlementId, options) options = options or {} local query = {} if options.scopeType ~= nil then table.insert(query, "scopeType=" .. HttpService:UrlEncode(tostring(options.scopeType))) end if options.scopeId ~= nil then table.insert(query, "scopeId=" .. HttpService:UrlEncode(tostring(options.scopeId))) end if options.universeId ~= nil then table.insert(query, "universeId=" .. HttpService:UrlEncode(tostring(options.universeId))) end if options.placeId ~= nil then table.insert(query, "placeId=" .. HttpService:UrlEncode(tostring(options.placeId))) end if options.groupId ~= nil then table.insert(query, "groupId=" .. HttpService:UrlEncode(tostring(options.groupId))) end local path = "/api/v1/entitlements/users/" .. tostring(userId) .. "/" .. HttpService:UrlEncode(tostring(entitlementId)) if #query > 0 then path = path .. "?" .. table.concat(query, "&") end return self:Request(path, "DELETE") end function RobloxAPIs:RunEntitlementSelfTest(userId) userId = userId or 156 local entitlementId = "sdk_vip_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000, 9999)) local granted, grantErr = self:GrantEntitlement(userId, entitlementId, { type = "vip", source = "RunEntitlementSelfTest", scopeType = "universe", scopeId = tostring(game.GameId), quantity = 1, metadata = { title = "SDK VIP test", source = "RunEntitlementSelfTest" } }) if grantErr then return nil, grantErr end local checked, checkErr = self:CheckEntitlements(userId, { entitlementId }, { mode = "all", universeId = game.GameId }) local listed, listErr = self:ListEntitlements(userId, { entitlementId = entitlementId, universeId = game.GameId, limit = 20 }) local revoked, revokeErr = self:RevokeEntitlement(userId, entitlementId, { scopeType = "universe", scopeId = tostring(game.GameId) }) local found = false if listed and listed.entitlements then for _, entitlement in ipairs(listed.entitlements) do if entitlement.entitlementId == entitlementId then found = true end end end return { ok = granted ~= nil and granted.entitlement ~= nil and checked ~= nil and checked.entitled == true and found and revoked ~= nil and revoked.revoked == true, entitlementId = entitlementId, granted = granted, checked = checked, listed = listed, revoked = revoked, errors = { check = checkErr, list = listErr, revoke = revokeErr } }, nil end function RobloxAPIs:SetInventoryItem(userId, itemId, item) return self:Request("/api/v1/inventory/users/" .. tostring(userId) .. "/items/" .. HttpService:UrlEncode(tostring(itemId)), "PUT", item or {}) end function RobloxAPIs:GetInventoryItem(userId, itemId) return self:Request("/api/v1/inventory/users/" .. tostring(userId) .. "/items/" .. HttpService:UrlEncode(tostring(itemId)), "GET") end function RobloxAPIs:UpdateInventoryItem(userId, itemId, patch) return self:Request("/api/v1/inventory/users/" .. tostring(userId) .. "/items/" .. HttpService:UrlEncode(tostring(itemId)), "PATCH", patch or {}) end function RobloxAPIs:DeleteInventoryItem(userId, itemId) return self:Request("/api/v1/inventory/users/" .. tostring(userId) .. "/items/" .. HttpService:UrlEncode(tostring(itemId)), "DELETE") end function RobloxAPIs:ListInventory(userId, options) options = options or {} local query = {} if options.status ~= nil then table.insert(query, "status=" .. HttpService:UrlEncode(tostring(options.status))) end if options.type ~= nil then table.insert(query, "type=" .. HttpService:UrlEncode(tostring(options.type))) end if options.itemType ~= nil then table.insert(query, "itemType=" .. HttpService:UrlEncode(tostring(options.itemType))) end if options.equipped ~= nil then table.insert(query, "equipped=" .. HttpService:UrlEncode(options.equipped and "1" or "0")) end if options.loadoutSlot ~= nil then table.insert(query, "loadoutSlot=" .. HttpService:UrlEncode(tostring(options.loadoutSlot))) end if options.includeInactive ~= nil then table.insert(query, "includeInactive=" .. HttpService:UrlEncode(options.includeInactive and "1" or "0")) end if options.limit ~= nil then table.insert(query, "limit=" .. HttpService:UrlEncode(tostring(options.limit))) end local path = "/api/v1/inventory/users/" .. tostring(userId) if #query > 0 then path = path .. "?" .. table.concat(query, "&") end return self:Request(path, "GET") end function RobloxAPIs:RunInventorySelfTest(userId) userId = userId or 156 local itemId = "sdk_sword_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000, 9999)) local saved, saveErr = self:SetInventoryItem(userId, itemId, { type = "weapon", quantity = 1, equipped = false, attributes = { rarity = "rare", power = 120 }, metadata = { source = "RunInventorySelfTest" } }) if saveErr then return nil, saveErr end local equipped, equipErr = self:UpdateInventoryItem(userId, itemId, { equipped = true, loadoutSlot = "primary", metadata = { equippedBy = "RunInventorySelfTest" } }) local fetched, fetchErr = self:GetInventoryItem(userId, itemId) local listed, listErr = self:ListInventory(userId, { type = "weapon", equipped = true, limit = 20 }) local deleted, deleteErr = self:DeleteInventoryItem(userId, itemId) local found = false if listed and listed.items then for _, item in ipairs(listed.items) do if item.itemId == itemId then found = true end end end return { ok = saved ~= nil and saved.item ~= nil and equipped ~= nil and equipped.item ~= nil and equipped.item.equipped == true and fetched ~= nil and fetched.item ~= nil and found and deleted ~= nil and deleted.deleted == true, itemId = itemId, saved = saved, equipped = equipped, fetched = fetched, listed = listed, deleted = deleted, errors = { equip = equipErr, fetch = fetchErr, list = listErr, delete = deleteErr } }, nil end function RobloxAPIs:SetQuestTemplate(templateKey, template) return self:Request("/api/v1/quests/templates/" .. HttpService:UrlEncode(tostring(templateKey)), "PUT", template or {}) end function RobloxAPIs:GetQuestTemplate(templateKey) return self:Request("/api/v1/quests/templates/" .. HttpService:UrlEncode(tostring(templateKey)), "GET") end function RobloxAPIs:ListQuestTemplates(options) options = options or {} local query = {} if options.season ~= nil then table.insert(query, "season=" .. HttpService:UrlEncode(tostring(options.season))) end if options.cadence ~= nil then table.insert(query, "cadence=" .. HttpService:UrlEncode(tostring(options.cadence))) end if options.tag ~= nil then table.insert(query, "tag=" .. HttpService:UrlEncode(tostring(options.tag))) end if options.includeInactive ~= nil then table.insert(query, "includeInactive=" .. HttpService:UrlEncode(options.includeInactive and "1" or "0")) end if options.limit ~= nil then table.insert(query, "limit=" .. HttpService:UrlEncode(tostring(options.limit))) end local path = "/api/v1/quests/templates" if #query > 0 then path = path .. "?" .. table.concat(query, "&") end return self:Request(path, "GET") end function RobloxAPIs:DeleteQuestTemplate(templateKey) return self:Request("/api/v1/quests/templates/" .. HttpService:UrlEncode(tostring(templateKey)), "DELETE") end function RobloxAPIs:AssignQuest(userId, templateKey, options) options = options or {} return self:Request("/api/v1/quests/assignments", "POST", { assignmentId = options.assignmentId, userId = userId, templateKey = templateKey, progress = options.progress, rewardsClaimed = options.rewardsClaimed, status = options.status or "active", expiresAt = options.expiresAt, metadata = options.metadata or {} }) end function RobloxAPIs:GetQuestAssignment(assignmentId) return self:Request("/api/v1/quests/assignments/" .. HttpService:UrlEncode(tostring(assignmentId)), "GET") end function RobloxAPIs:ListQuestAssignments(userId, options) options = options or {} local query = {} if options.season ~= nil then table.insert(query, "season=" .. HttpService:UrlEncode(tostring(options.season))) end if options.status ~= nil then table.insert(query, "status=" .. HttpService:UrlEncode(tostring(options.status))) end if options.templateKey ~= nil then table.insert(query, "templateKey=" .. HttpService:UrlEncode(tostring(options.templateKey))) end if options.includeDeleted ~= nil then table.insert(query, "includeDeleted=" .. HttpService:UrlEncode(options.includeDeleted and "1" or "0")) end if options.limit ~= nil then table.insert(query, "limit=" .. HttpService:UrlEncode(tostring(options.limit))) end local path = "/api/v1/quests/assignments/users/" .. tostring(userId) if #query > 0 then path = path .. "?" .. table.concat(query, "&") end return self:Request(path, "GET") end function RobloxAPIs:UpdateQuestAssignment(assignmentId, patch) return self:Request("/api/v1/quests/assignments/" .. HttpService:UrlEncode(tostring(assignmentId)), "PATCH", patch or {}) end function RobloxAPIs:DeleteQuestAssignment(assignmentId) return self:Request("/api/v1/quests/assignments/" .. HttpService:UrlEncode(tostring(assignmentId)), "DELETE") end function RobloxAPIs:RunQuestTemplateSelfTest(userId) userId = userId or 156 local templateKey = "sdk_daily_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000, 9999)) local assignmentId = "sdk_assignment_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000, 9999)) local saved, saveErr = self:SetQuestTemplate(templateKey, { season = "sdk-season", cadence = "daily", title = "SDK Daily Win", description = "Complete a daily objective through RobloxAPIs.", objectives = { { key = "win_round", title = "Win two rounds", metric = "round_win", target = 2 } }, rewards = { { key = "coins", type = "currency", amount = 100 } }, tags = { "sdk", "daily" } }) if saveErr then return nil, saveErr end local assigned, assignErr = self:AssignQuest(userId, templateKey, { assignmentId = assignmentId, metadata = { source = "RunQuestTemplateSelfTest" } }) local patched, patchErr = self:UpdateQuestAssignment(assignmentId, { objectiveKey = "win_round", progress = 2, increment = false, claimedReward = "coins" }) local listed, listErr = self:ListQuestAssignments(userId, { templateKey = templateKey, limit = 20 }) local deletedAssignment, deleteAssignmentErr = self:DeleteQuestAssignment(assignmentId) local deletedTemplate, deleteTemplateErr = self:DeleteQuestTemplate(templateKey) local found = false if listed and listed.assignments then for _, assignment in ipairs(listed.assignments) do if assignment.assignmentId == assignmentId then found = true end end end return { ok = saved ~= nil and assigned ~= nil and assigned.assignment ~= nil and patched ~= nil and patched.assignment ~= nil and patched.assignment.complete == true and found and deletedAssignment ~= nil and deletedAssignment.deleted == true and deletedTemplate ~= nil and deletedTemplate.deleted == true, templateKey = templateKey, assignmentId = assignmentId, saved = saved, assigned = assigned, patched = patched, listed = listed, deletedAssignment = deletedAssignment, deletedTemplate = deletedTemplate, errors = { assign = assignErr, patch = patchErr, list = listErr, deleteAssignment = deleteAssignmentErr, deleteTemplate = deleteTemplateErr } }, nil end function RobloxAPIs:SetProgression(userId, profile) return self:Request("/api/v1/progression/users/" .. tostring(userId), "PUT", profile or {}) end function RobloxAPIs:UpdateProgression(userId, patch) return self:Request("/api/v1/progression/users/" .. tostring(userId), "PATCH", patch or {}) end function RobloxAPIs:GetProgression(userId, season) local path = "/api/v1/progression/users/" .. tostring(userId) if season ~= nil then path = path .. "?season=" .. HttpService:UrlEncode(tostring(season)) end return self:Request(path, "GET") end function RobloxAPIs:DeleteProgression(userId, season) local path = "/api/v1/progression/users/" .. tostring(userId) if season ~= nil then path = path .. "?season=" .. HttpService:UrlEncode(tostring(season)) end return self:Request(path, "DELETE") end function RobloxAPIs:RunProgressionSelfTest(userId) userId = userId or 156 local season = "sdk-test-" .. tostring(os.time()) local saved, saveErr = self:SetProgression(userId, { season = season, level = 1, xp = 0, streak = 0, quests = { daily_win = { progress = 0, goal = 2 } } }) if saveErr then return nil, saveErr end local patched, patchErr = self:UpdateProgression(userId, { season = season, quest = "daily_win", progress = 2, goal = 2, xp = 150, incrementXp = true, claimedReward = "daily_win_reward" }) local fetched, fetchErr = self:GetProgression(userId, season) return { ok = patched ~= nil and patched.profile ~= nil and patched.profile.quests ~= nil and patched.profile.quests.daily_win ~= nil and patched.profile.quests.daily_win.completed == true and fetched ~= nil and fetched.found == true, season = season, saved = saved, patched = patched, fetched = fetched, errors = { patch = patchErr, fetch = fetchErr } }, nil end function RobloxAPIs:SendWebhookEvent(eventType, payload) return self:Request("/api/v1/webhooks/events", "POST", { type = eventType, payload = payload or {} }) end function RobloxAPIs:PublishLiveMessage(topic, payload, options) options = options or {} return self:Request("/api/v1/live-messages/publish", "POST", { messageId = options.messageId, topic = topic, payload = payload or {}, audience = options.audience or {}, universeId = options.universeId or game.GameId, serverJobId = options.serverJobId, priority = options.priority or "normal", ttlSeconds = options.ttlSeconds or 600 }) end function RobloxAPIs:PollLiveMessages(options) options = options or {} local query = {} if options.topic ~= nil then table.insert(query, "topic=" .. HttpService:UrlEncode(tostring(options.topic))) end if options.since ~= nil then table.insert(query, "since=" .. HttpService:UrlEncode(tostring(options.since))) end if options.universeId ~= nil then table.insert(query, "universeId=" .. HttpService:UrlEncode(tostring(options.universeId))) end if options.serverJobId ~= nil then table.insert(query, "serverJobId=" .. HttpService:UrlEncode(tostring(options.serverJobId))) end if options.limit ~= nil then table.insert(query, "limit=" .. HttpService:UrlEncode(tostring(options.limit))) end local path = "/api/v1/live-messages/poll" if #query > 0 then path = path .. "?" .. table.concat(query, "&") end return self:Request(path, "GET") end function RobloxAPIs:GetLiveMessage(messageId) return self:Request("/api/v1/live-messages/" .. HttpService:UrlEncode(tostring(messageId)), "GET") end function RobloxAPIs:RevokeLiveMessage(messageId) return self:Request("/api/v1/live-messages/" .. HttpService:UrlEncode(tostring(messageId)), "DELETE") end function RobloxAPIs:RunLiveMessagingSelfTest() local topic = "sdk.self-test" local messageId = "sdk_msg_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000, 9999)) local published, publishErr = self:PublishLiveMessage(topic, { title = "Roblox APIs self-test", body = "Live Messaging is available.", enabled = true }, { messageId = messageId, priority = "high", ttlSeconds = 300 }) if publishErr then return nil, publishErr end local polled, pollErr = self:PollLiveMessages({ topic = topic, limit = 20 }) local lookup, lookupErr = self:GetLiveMessage(messageId) local revoked, revokeErr = self:RevokeLiveMessage(messageId) local found = false if polled and polled.messages then for _, message in ipairs(polled.messages) do if message.messageId == messageId then found = true end end end return { ok = published ~= nil and published.duplicate == false and found and lookup ~= nil and lookup.message ~= nil and revoked ~= nil and revoked.revoked == true, messageId = messageId, published = published, polled = polled, lookup = lookup, revoked = revoked, errors = { poll = pollErr, lookup = lookupErr, revoke = revokeErr } }, nil end function RobloxAPIs:CreateMatchmakingTicket(userId, options) options = options or {} return self:Request("/api/v1/matchmaking/tickets", "POST", { ticketId = options.ticketId, userId = userId, partyId = options.partyId, party = options.party or { { userId = userId, role = "leader", ready = true } }, mode = options.mode or "default", region = options.region or "auto", skill = options.skill or 0, autoMatch = options.autoMatch, ttlSeconds = options.ttlSeconds or 300, metadata = options.metadata or {} }) end function RobloxAPIs:GetMatchmakingTicket(ticketId) return self:Request("/api/v1/matchmaking/tickets/" .. HttpService:UrlEncode(tostring(ticketId)), "GET") end function RobloxAPIs:ListMatchmakingTickets(options) options = options or {} local query = {} if options.status ~= nil then table.insert(query, "status=" .. HttpService:UrlEncode(tostring(options.status))) end if options.mode ~= nil then table.insert(query, "mode=" .. HttpService:UrlEncode(tostring(options.mode))) end if options.userId ~= nil then table.insert(query, "userId=" .. HttpService:UrlEncode(tostring(options.userId))) end if options.limit ~= nil then table.insert(query, "limit=" .. HttpService:UrlEncode(tostring(options.limit))) end local path = "/api/v1/matchmaking/tickets" if #query > 0 then path = path .. "?" .. table.concat(query, "&") end return self:Request(path, "GET") end function RobloxAPIs:CancelMatchmakingTicket(ticketId) return self:Request("/api/v1/matchmaking/tickets/" .. HttpService:UrlEncode(tostring(ticketId)), "DELETE") end function RobloxAPIs:HeartbeatServer(options) options = options or {} local defaultServerId = game.JobId if defaultServerId == nil or defaultServerId == "" then defaultServerId = "studio-server-" .. tostring(os.time()) .. "-" .. tostring(math.random(1000, 9999)) end local playerCount = options.playerCount or options.players or #Players:GetPlayers() local maxPlayers = options.maxPlayers or options.capacity or Players.MaxPlayers return self:Request("/api/v1/servers/heartbeat", "POST", { serverId = options.serverId or options.serverJobId or options.jobId or defaultServerId, universeId = options.universeId or game.GameId, placeId = options.placeId or game.PlaceId, jobId = options.jobId or options.serverJobId or game.JobId, mode = options.mode or "default", region = options.region or "auto", playerCount = playerCount, maxPlayers = maxPlayers, status = options.status or "active", ttlSeconds = options.ttlSeconds or 180, metadata = options.metadata or {} }) end function RobloxAPIs:ListServers(options) options = options or {} local query = {} if options.status ~= nil then table.insert(query, "status=" .. HttpService:UrlEncode(tostring(options.status))) end if options.mode ~= nil then table.insert(query, "mode=" .. HttpService:UrlEncode(tostring(options.mode))) end if options.region ~= nil then table.insert(query, "region=" .. HttpService:UrlEncode(tostring(options.region))) end if options.universeId ~= nil then table.insert(query, "universeId=" .. HttpService:UrlEncode(tostring(options.universeId))) end if options.placeId ~= nil then table.insert(query, "placeId=" .. HttpService:UrlEncode(tostring(options.placeId))) end if options.limit ~= nil then table.insert(query, "limit=" .. HttpService:UrlEncode(tostring(options.limit))) end if options.includeExpired ~= nil then table.insert(query, "includeExpired=" .. HttpService:UrlEncode(options.includeExpired and "1" or "0")) end local path = "/api/v1/servers" if #query > 0 then path = path .. "?" .. table.concat(query, "&") end return self:Request(path, "GET") end function RobloxAPIs:GetServer(serverId) return self:Request("/api/v1/servers/" .. HttpService:UrlEncode(tostring(serverId)), "GET") end function RobloxAPIs:RemoveServer(serverId) return self:Request("/api/v1/servers/" .. HttpService:UrlEncode(tostring(serverId)), "DELETE") end function RobloxAPIs:RunServerDirectorySelfTest() local serverId = "sdk_server_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000, 9999)) local heartbeat, heartbeatErr = self:HeartbeatServer({ serverId = serverId, mode = "sdk-test", region = "auto", playerCount = 1, maxPlayers = 12, ttlSeconds = 300, metadata = { source = "RunServerDirectorySelfTest" } }) if heartbeatErr then return nil, heartbeatErr end local lookup, lookupErr = self:GetServer(serverId) local listed, listErr = self:ListServers({ mode = "sdk-test", limit = 20 }) local removed, removeErr = self:RemoveServer(serverId) local found = false if listed and listed.servers then for _, server in ipairs(listed.servers) do if server.serverId == serverId then found = true end end end return { ok = heartbeat ~= nil and heartbeat.server ~= nil and lookup ~= nil and lookup.server ~= nil and found and removed ~= nil and removed.removed == true, serverId = serverId, heartbeat = heartbeat, lookup = lookup, listed = listed, removed = removed, errors = { lookup = lookupErr, list = listErr, remove = removeErr } }, nil end function RobloxAPIs:RunMatchmakingSelfTest(userId) userId = userId or 156 local ticketId = "sdk_ticket_" .. tostring(os.time()) .. "_" .. tostring(math.random(1000, 9999)) local created, createErr = self:CreateMatchmakingTicket(userId, { ticketId = ticketId, mode = "ranked_2v2", region = "auto", skill = 1420, party = { { userId = userId, role = "leader", ready = true } }, metadata = { source = "RunMatchmakingSelfTest" } }) if createErr then return nil, createErr end local lookup, lookupErr = self:GetMatchmakingTicket(ticketId) local listed, listErr = self:ListMatchmakingTickets({ mode = "ranked_2v2", limit = 20 }) local cancelled, cancelErr = self:CancelMatchmakingTicket(ticketId) local found = false if listed and listed.tickets then for _, ticket in ipairs(listed.tickets) do if ticket.ticketId == ticketId then found = true end end end return { ok = created ~= nil and created.duplicate == false and created.ticket ~= nil and created.ticket.status == "matched" and lookup ~= nil and lookup.ticket ~= nil and found and cancelled ~= nil and cancelled.cancelled == true, ticketId = ticketId, created = created, lookup = lookup, listed = listed, cancelled = cancelled, errors = { lookup = lookupErr, list = listErr, cancel = cancelErr } }, nil end function RobloxAPIs:SetGroupPolicy(groupId, policy) return self:Request("/api/v1/groups/" .. tostring(groupId) .. "/policy", "PATCH", policy or {}) end function RobloxAPIs:GetGroupPolicy(groupId) return self:Request("/api/v1/groups/" .. tostring(groupId) .. "/policy", "GET") end function RobloxAPIs:ModerateJoin(player, context) context = context or {} context.userId = player.UserId context.universeId = context.universeId or game.GameId context.serverJobId = context.serverJobId or game.JobId local result, err = self:CheckBan(player.UserId, context) if err then warn("RobloxAPIs moderation check failed", err.statusCode) return false, err end if result and result.banned then player:Kick(context.kickMessage or "Moderation action active") return true, result end return false, result end function RobloxAPIs:RunStudioSelfTests(options) options = options or {} local userId = options.userId or 156 local stamp = tostring(os.time()) .. "-" .. tostring(math.random(1000, 9999)) local summary = { ok = true, userId = userId, startedAt = os.date("!%Y-%m-%dT%H:%M:%SZ"), tests = {} } local function record(name, result, err) local passed = err == nil and result ~= nil and result.ok ~= false summary.tests[name] = { ok = passed, result = result, error = err } if not passed then summary.ok = false warn("RobloxAPIs self-test failed:", name, err and (err.statusCode or err.statusMessage) or "unknown") else print("RobloxAPIs self-test passed:", name) end end local configKey = "studio-self-test-" .. stamp local config, configErr = self:SetExperienceConfig(configKey, { title = "Roblox APIs Self-Test", message = "Config, storage, moderation, trades, receipts, progression, economy, and events are running.", accent = "#20E3B2", variant = "success" }) record("experienceConfigWrite", config, configErr) if options.renderSign ~= false then local rendered, renderErr = self:ApplyExperienceConfigSign(configKey, { partName = options.partName or "RobloxAPIs_SelfTest", position = options.position or Vector3.new(0, 8, -14) }) record("experienceConfigRender", rendered, renderErr) end local objectKey = "studio-self-test/" .. stamp local stored, storeErr = self:PutObject(objectKey, { userId = userId, source = "RunStudioSelfTests", createdAt = summary.startedAt }) record("storageVaultWrite", stored, storeErr) local fetchedObject, fetchObjectErr = self:GetObject(objectKey) record("storageVaultRead", fetchedObject, fetchObjectErr) local crossBan, crossBanErr = self:RunCrossBanSelfTest(userId, { reason = "RunStudioSelfTests temporary scoped ban" }) record("crossBan", crossBan, crossBanErr) local receipt, receiptErr = self:RunReceiptSelfTest(userId, options.productId or 100001, "studio_self_test_entitlement") record("receiptLedger", receipt, receiptErr) local entitlement, entitlementErr = self:RunEntitlementSelfTest(userId) record("entitlementMirror", entitlement, entitlementErr) local inventory, inventoryErr = self:RunInventorySelfTest(userId) record("inventorySnapshot", inventory, inventoryErr) local questTemplates, questTemplatesErr = self:RunQuestTemplateSelfTest(userId) record("questTemplates", questTemplates, questTemplatesErr) local progression, progressionErr = self:RunProgressionSelfTest(userId) record("progressionCloud", progression, progressionErr) local trade, tradeErr = self:RunTradeSelfTest(userId, options.tradePartnerUserId or 1) record("tradeGuard", trade, tradeErr) local groupId = options.groupId or 123456 local groupPolicy, groupPolicyErr = self:SetGroupPolicy(groupId, { enforceCrossGameBans = true, trustedRoles = { "Admin", "Moderator" }, linkedUniverses = { tostring(game.GameId) }, notes = "RunStudioSelfTests sample group policy" }) record("groupSync", groupPolicy, groupPolicyErr) local economy, economyErr = self:SendEconomySignal(userId, "studio_self_test", 25, { velocity = 1, source = "RunStudioSelfTests" }) record("economyGuard", economy, economyErr) local analytics, analyticsErr = self:RunAnalyticsSelfTest(userId) record("sessionAnalytics", analytics, analyticsErr) local featureFlags, featureFlagsErr = self:RunFeatureFlagsSelfTest(userId) record("featureFlags", featureFlags, featureFlagsErr) local webhook, webhookErr = self:SendWebhookEvent("studio.self_test", { userId = userId, source = "RunStudioSelfTests" }) record("webhookEvents", webhook, webhookErr) local liveMessage, liveMessageErr = self:RunLiveMessagingSelfTest() record("liveMessaging", liveMessage, liveMessageErr) local matchmaking, matchmakingErr = self:RunMatchmakingSelfTest(userId) record("matchmakingQueue", matchmaking, matchmakingErr) local serverDirectory, serverDirectoryErr = self:RunServerDirectorySelfTest() record("serverDirectory", serverDirectory, serverDirectoryErr) local deleted, deleteErr = self:DeleteObject(objectKey) record("storageVaultCleanup", deleted, deleteErr) summary.finishedAt = os.date("!%Y-%m-%dT%H:%M:%SZ") print("RobloxAPIs self-test complete:", summary.ok) return summary, nil end return RobloxAPIs