Group ranking with Roblox OpenCloud!

Introduction

Ever wanted to rank people in your group without NodeJS, Noblox, or a third party service?
Well now you can, with the introduction of [Beta] Use Open Cloud via HttpService Without Proxies!
It’s a bit complicated to setup, but it’s worth it in the future!

Small note:

  • API keys will need to be encrypted in Base 64 for Roblox Studio only, which can be done in a third party website.
  • It’s currently in beta, and the code I provided is quick sample code I made. Not the best, but it works!

Setup:

1st Step: Creating the OpenCloud API Key!

Step 1: Creating the API Key!

You’ll have to first create a OpenCloud API Key on Roblox!
You can do this by heading to the OpenCloud API Keys dashboard!
Make sure you’re on a PROFILE and not a group, group API keys are currently bugged!
Don’t worry as well, API Keys made on a profile will work in group games!

Step 2: Naming your API key.

You’ll want to give your key a name, this can be anything.
It doesn’t really matter as we’ll set it as a different secret name later in the game you wish to rank people in.

Step 3: API Access Permissions

We’ll only need group:read and group:write!

Step 4: Accepted IP addresses.

You’ll have to set the IP as 0.0.0.0/0 for right now.
There might be a different option in the future as stated in this post.

Step 5: Copying your API key!

This is needed, as we’ll be encrypting the key in Base 64!

2nd Step: Encrypting your API Key to Base64 for Roblox Studio!

I personally like using CyberChef for encryption!
Any other Base64 encryption website will work though! :raised_hands:
Make sure you have it set to To Base64! This is needed for Roblox Studio Secrets as stated here.

3rd step: Creating the secret!

Note:

We’ll have to set the secret in two places!
First in the main experience, and the next in studio!

1st place: Setting the secret on the website!

Head to the secret tab in your experience page!

Make sure the key you’re adding in is the original key! [Not base 64 enocded.]
And that’s all you need to do for the website!

2nd place: Setting the secret in Roblox Studio!

Head to GameSettings → Security tab!
This is where we’ll set our Secret in studio!
This key will need to be in Base64!

We’ll use a template like in this documentation to make the API key to work in studio!
{"secretName": ["YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", "*.roblox.com"]}
Should look something like this after we add it in!
Make sure the secretName is changed to the same secret name on the website!

4th step: Adding/editing the sample code!

I made a ModuleScript for Group ranking!
If you set up the secret correctly, and Group ID in the script, it should work!
Make sure the API key name is right in the code as well!
It could be different if you set it differently in step 3! :3

This code was made quickly, so it’s not the best quality.
Marketplace sample moduleScript!

Modulescript Code in text:
--!strict
--[[
	Name: Group OpenCloud Module
	Author: va0ck
	Description: Wrapper for OpenCloud group!
	Date created: 2025/06/02
	
	Instructions:
		1. Enable HttpService!
		2. Create a OpenClould API key from a USER ACCOUNT. 				 	[Group keys are broke at the moment!]
		    Make sure to allow group:write!
		    Also make sure to allow 0.0.0.0/0!
		     
		3. Encode the OpenCloud API key in Base64! 							 	[Cyberchef is helpful for Base64!]
		4. Create a secret in both Studio & Roblox Website with the API key! 	[Studio for local development, Website for live games!]
		5. You should be good to edit the rank settings! :D
]]--
----------------
--//Settings//--
----------------
local GROUP_ID = 0
local SECRET_KEY_NAME = "roblox_cloud_key"
local MAX_RETRY_ATTEMPTS = 3

----------------
--//Services//--
----------------
local HttpService = game:GetService("HttpService")

------------------
--//Main Module//-
-------------------
local GroupCloudModule = {}
GroupCloudModule.API_KEY = HttpService:GetSecret(SECRET_KEY_NAME)
type RankIdTableType =  {rankId: number, rank: number}
type RankIdsListType = {RankIdTableType}
GroupCloudModule.CACHED_RankIds = {} :: RankIdsListType

--------------------------------
--//Base OpenCloud Functions//--
--------------------------------
local function getRobloxAPI(url: string)
	local success, response = pcall(function()
		return HttpService:RequestAsync({
			Url = url,
			Method = "GET",
			Headers = {
				["x-api-key"] = GroupCloudModule.API_KEY,
				["Content-Type"] = "application/json",
			},
		})
	end)
	return success, response
end
local function postOrPatchRobloxAPI(url:string, method: "POST"|"PATCH", data: {string})
	local success, response = pcall(function()
		return HttpService:RequestAsync({
			Url = url,
			Method = method,
			Headers = {
				["x-api-key"] = GroupCloudModule.API_KEY,
				["Content-Type"] = "application/json",
			},
			Body = HttpService:JSONEncode(data),
		})
	end)
	return success, response
end
local function requestRobloxAPI(url: string, method: string, data: {string}?)
	local attemptIndex = 0
	local success, response
	repeat
		if method == "GET" then
			success, response = getRobloxAPI(url)
		elseif (method == "PATCH" or method == "POST") then
			assert(data, "Data is required for Patch/Post to Roblox!")
			success, response = postOrPatchRobloxAPI(url, method, data)
		end
		
		if (not success) then task.wait(1) end
		attemptIndex += 1
	until success or attemptIndex >= MAX_RETRY_ATTEMPTS
	return success, response
end

--------------------------------------
--//Ranking API Wrapper Functions//--
-------------------------------------
local function getMembershipId(userId: number): (boolean, string)
	local membershipFilter = `user == 'users/{userId}'`
	local searchSuccess, responseBody = requestRobloxAPI(
		`https://apis.roblox.com/cloud/v2/groups/{GROUP_ID}/memberships?maxPageSize=10&filter={membershipFilter}`,
		"GET"
	)
	responseBody = HttpService:JSONDecode(responseBody.Body)
	if searchSuccess and responseBody.groupMemberships and 
		responseBody.groupMemberships[1] and 
		responseBody.groupMemberships[1].path 
	then
		--Path format: groups/{group_id}/memberships/{group_membership_id}
		local unfilteredText = responseBody.groupMemberships[1].path
		unfilteredText = string.split(unfilteredText, "/")
		return true, unfilteredText[4]
	else
		return false, responseBody
	end
end
local function getGroupRankIds(clearCache: boolean?): (boolean, string | RankIdsListType)
	if #GroupCloudModule.CACHED_RankIds > 0 and (not clearCache) then
		return true, GroupCloudModule.CACHED_RankIds 
	end
	
	--Clear cache of rankIds.
	table.clear(GroupCloudModule.CACHED_RankIds)
	local UNCLEANED_ROLES = {}
	local rolesSuccess, rolesBody = requestRobloxAPI(
		`https://apis.roblox.com/cloud/v2/groups/{GROUP_ID}/roles?maxPageSize=20`,
		"GET"
	)
	rolesBody = HttpService:JSONDecode(rolesBody['Body'])
	if rolesSuccess and rolesBody.groupRoles then
		for _,v in pairs(rolesBody.groupRoles) do
			table.insert(UNCLEANED_ROLES, v)
		end
		while rolesBody.nextPageToken ~= "" do
			rolesSuccess, rolesBody = requestRobloxAPI(
				`https://apis.roblox.com/cloud/v2/groups/{GROUP_ID}/roles?maxPageSize=20&pageToken={rolesBody.nextPageToken}`,
				"GET"
			)
			if rolesSuccess and rolesBody.groupRoles then
				for _,v in pairs(rolesBody.groupRoles) do
					table.insert(UNCLEANED_ROLES, v)
				end
			else
				return false, "Could not get groupRoles nextPageToken reponse body!"
			end
		end
		for _,v in pairs(UNCLEANED_ROLES) do
			table.insert(GroupCloudModule.CACHED_RankIds, {
				rankId = v.id,
				rank = v.rank
			})
		end
		table.sort(GroupCloudModule.CACHED_RankIds, function(a, b)
			return a.rank < b.rank
		end)
		return true, GroupCloudModule.CACHED_RankIds
	end
	return false, "Could not get first groupRoles reponse body!"
end
local function getRankNumRoleId(groupRoles: any | RankIdsListType, rankNum: number): (boolean, number | string)
	for _,v in pairs(groupRoles) do
		if v.rank == rankNum then
			return true, v.rankId
		end
	end
	--Clear GroupRankIdsCache with new set!
	local _, _ = getGroupRankIds(true)
	return false, "RankNum did not exist in group roles! (Retrieved new roleIds, try again!)"
end
local function updatePlayerMembership(membershipId: string, roleId: number | string)
	local updateSuccess, updateResponse = requestRobloxAPI(
		`https://apis.roblox.com/cloud/v2/groups/{GROUP_ID}/memberships/{membershipId}`,
		"PATCH",
		{ role = `groups/{GROUP_ID}/roles/{roleId}` }
	)
	return updateSuccess, `Could not update player membership: {membershipId}`
end

----------------------
--//Main Functions//--
----------------------
GroupCloudModule.UpdateRank = function(userId: number, rankNum: number): (boolean, string)
	local membershipIdSucces, membershipId = getMembershipId(userId)
	if (not membershipIdSucces) then return membershipIdSucces, membershipId end
	local groupRolesSuccess, groupRoles = getGroupRankIds()
	if (not groupRolesSuccess) then return groupRolesSuccess, groupRoles:: string end
	local rankRoleIdSuccess, rankRoleId = getRankNumRoleId(groupRoles, rankNum)
	if (not rankRoleIdSuccess) then return rankRoleIdSuccess, rankRoleId:: string end
	local updateSuccess, updateResponse = updatePlayerMembership(membershipId, rankRoleId)
	if (not updateSuccess) then return updateSuccess, updateResponse end
	return true, `Updated role for {userId}!`
end

return GroupCloudModule

Final step: Testing!

Here’s small test code if you’re using my sample Modulescript!

--!strict
local GroupCloud = require(script.GroupCloud)

local rankSuccess, rankMsg = GroupCloud.UpdateRank(userId, rankId)
if (not rankSuccess) then
	warn(rankMsg)
else
	print(rankMsg)
end

And tada! You’ll be able to rank the user from within Roblox!




Code explanation (Optional read):

Services, types, and table used:

GROUP_ID: The group you want to rank people in.
SECRET_KEY_NAME: The name you have for both Studio and the website.
MAX_RETRY_ATTEMPT: If HttpRequest fails, it’ll wait a second and try again.

type RankIdTableType: {
rankId: The unique roleId on the website.
rank: The role number in the group.
}

GroupCloudModule.CACHED_RankIds: Roles cached so HttpRequest isn’t spammed.

Main request function!

These are made to reduce the code count,
we also include the secret into the HttpRequest headers.

Grabbing the membershipId!

First part of wanting to rank someone will be needing to get the membershipId!
This is done by using List Group Memberships and filtering it to one user.

Grabbing the group roles!

Since the roles on the website is different from the rank number,
we need to get the unique identifier of the roles!
This is done by grabbing the roles all at once and caching it with List Group Roles!

Checking the rank number!

We’ll check the Cached group roles to see if it matches
with the rank number we want to update the user with!

If it doesn’t exist, we’ll clear the list for the roles to be all cached again.

Updating the player rank!

To do this, we would need the user membershipId and website roleId!
This was all retrieved earlier in the process!


Helpful resources!

Modulescript Code | Marketplace!
HttpService Announcement | Devforum!
HttpService | Documentation!
Group API | Documentation!
Secrets stores | Documentation!

10 Likes

I had been waiting so long for roblox to officially add API for it.
Thank you so much for making this tutorial.
Its gonna be insanely cool to rank people in group based on their ingame rank!

1 Like

@va0ck Hey, thanks for making this awesome tutorial! I just wanted to point out that this feature actually does work outside of Studio.

In Group ranking with Roblox OpenCloud!, the key you add in the website should be the exact same key you get after creating the API key (should not be base64 encoded).

In Group ranking with Roblox OpenCloud!, the key you add in Studio here should be base64 encoded.

I know it can be pretty confusing but we are working on making the Studio secret process better. Thanks for your patience!

6 Likes

Ah, thank you for the information!
Yeah, I had no idea about the studio key & website key difference. :sweat_smile:
Ty again!

1 Like

It still isn’t working for me even when I put normal API key for dashboard but studio is base 64

Hey! I don’t know if this is like this for other Users.

But there seems to be a bug at line 130 in the OpenCloud Module.
I fixed it by doing rolesBody = HttpService:JSONDecode(rolesBody['Body']) just before the if statement.

2 Likes

aww but it dosent work for me.. i tried it in every way

What error do you see when it doesn’t work?

it just says that uh something is wrong, couldnt find key, experienced error while loading module

Could you provide a screenshot or paste the exact error message you get? That’ll make it easier for me to figure out what is causing it, thanks!

Using Open Cloud directly from your game can be a bit unreliable because the API is still in beta and not always fully stable. This means it can be inconsistent and sometimes cause errors if the API is down, slow, or experiencing issues. Another common problem with Open Cloud is rate limits. If you send too many requests too quickly, you can get throttled or blocked, which breaks the flow of your game. Instead, I recommend building your own API with a Noblox fallback. With your own backend, you can queue operations and manage request rates to avoid hitting limits and ensure smoother performance. If Open Cloud runs into problems, your backend automatically switches to Noblox and keeps everything running smoothly.

On top of that, having your own API lets you set a dedicated IP address for your site and configure Open Cloud to only accept requests from that IP, giving you better control and security.

Plus, when you own the API, you can add your own validation, logging, rate limits, and customize how rank changes happen exactly how you want, which makes debugging and maintenance much easier.

Using TypeScript or Node.js for your backend opens up a lot of possibilities. You get strong typing, excellent tooling, easier upkeep, and the ability to integrate with other services seamlessly.

Your backend can also batch requests and handle more load than relying on client calls, which improves performance.

Overall, having a Noblox fallback ensures your system will not break and players will not get stuck if Open Cloud is acting up.

You also gain full control over monitoring and error tracking, which helps keep things running smoothly long term.

I definitely recommend going with your own API plus fallback; it is a more stable, professional setup. That said, it’s just my opinion and everyone has their own pros and cons. You do you.

1 Like

Needs fixes, heres mine
ln 123

		while rolesBody.nextPageToken ~= "" do
			rolesSuccess, rolesBody = requestRobloxAPI(
				`https://apis.roblox.com/cloud/v2/groups/{GROUP_ID}/roles?maxPageSize=20&pageToken={rolesBody.nextPageToken}`,
				"GET"
			)
			if rolesSuccess and rolesBody and rolesBody.groupRoles == nil then
				rolesBody = HttpService:JSONDecode(rolesBody.Body)	
			end
			
			if rolesSuccess and rolesBody.groupRoles then
				for _,v in pairs(rolesBody.groupRoles) do
					table.insert(UNCLEANED_ROLES, v)
				end
			else
				return false, "Could not get groupRoles nextPageToken reponse body!"
			end
		end
1 Like

the previous script that op linked didnt work, ill try this one

it still dosent work… sigh. im so sad